diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 00000000000..041f95abab6 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "x7-dental" + } +} diff --git a/Dockerfile b/Dockerfile index 289a7f1869e..e9a0e62dff6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -60,7 +60,7 @@ COPY --parents ./addOns/package.json ./addOns/*/*/package.json ./extensions/*/pa RUN bun pm cache rm RUN bun install -RUN bun add ajv@8.12.0 +RUN npm install --no-save --legacy-peer-deps ajv@8.12.0 # Copy the local directory COPY --link --exclude=yarn.lock --exclude=package.json --exclude=Dockerfile . . @@ -72,6 +72,20 @@ ARG APP_CONFIG=config/default.js ARG PUBLIC_URL=/ ENV PUBLIC_URL=${PUBLIC_URL} +# Firebase env vars β€” passed as --build-arg at docker build time +ARG REACT_APP_FIREBASE_API_KEY +ARG REACT_APP_FIREBASE_AUTH_DOMAIN +ARG REACT_APP_FIREBASE_PROJECT_ID +ARG REACT_APP_FIREBASE_STORAGE_BUCKET +ARG REACT_APP_FIREBASE_MESSAGING_SENDER_ID +ARG REACT_APP_FIREBASE_APP_ID +ENV REACT_APP_FIREBASE_API_KEY=${REACT_APP_FIREBASE_API_KEY} +ENV REACT_APP_FIREBASE_AUTH_DOMAIN=${REACT_APP_FIREBASE_AUTH_DOMAIN} +ENV REACT_APP_FIREBASE_PROJECT_ID=${REACT_APP_FIREBASE_PROJECT_ID} +ENV REACT_APP_FIREBASE_STORAGE_BUCKET=${REACT_APP_FIREBASE_STORAGE_BUCKET} +ENV REACT_APP_FIREBASE_MESSAGING_SENDER_ID=${REACT_APP_FIREBASE_MESSAGING_SENDER_ID} +ENV REACT_APP_FIREBASE_APP_ID=${REACT_APP_FIREBASE_APP_ID} + RUN bun run show:config RUN bun run build diff --git a/README.md b/README.md index a6d08f4bd0a..e3b7cd65d57 100644 --- a/README.md +++ b/README.md @@ -1,401 +1,280 @@ -
-

OHIF Medical Imaging Viewer

-

The OHIF Viewer is a zero-footprint medical image viewer -provided by the Open Health Imaging Foundation (OHIF). It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support DICOMweb.

-
- -
- Read The Docs -
-
- Live Demo | - Component Library -
-
- πŸ“° Join OHIF Newsletter πŸ“° -
-
- πŸ“° Join OHIF Newsletter πŸ“° -
+# 24x7 Dental SaaS Platform +### OHIF Viewer β€” Dental Mode Customization +**Built on top of the open-source [OHIF Medical Imaging Viewer](https://ohif.org/)** +A zero-footprint, browser-based DICOM viewer extended with a fully custom Dental Mode β€” featuring dental-specific measurements, a themed UI, tooth selection, Google authentication, and cloud deployment via Docker and Firebase. +--- -
+[![Live Demo](https://img.shields.io/badge/Live%20Demo-x7--dental.web.app-blue?style=for-the-badge&logo=firebase)](https://ohif-dental-viewer-962047575449.us-central1.run.app) +[![GitHub](https://img.shields.io/badge/GitHub-bitbossing%2F24x7--ohif-181717?style=for-the-badge&logo=github)](https://github.com/bitbossing/24x7-ohif) +[![OHIF](https://img.shields.io/badge/Based%20on-OHIF%20Viewers-green?style=for-the-badge)](https://ohif.org/) +[![License](https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge)](LICENSE) -[![NPM version][npm-version-image]][npm-url] -[![MIT License][license-image]][license-url] -[![This project is using Percy.io for visual regression testing.][percy-image]](percy-url) - - - +--- - - - - - +*April 13, 2026 β€” Dennis Jayvee Patricio* + -| | | | -| :-: | :--- | :--- | -| Measurement tracking | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) | -| Segmentations | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) | -| Hanging Protocols | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) | -| Volume Rendering | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) | -| PDF | PDF | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.317377619501274872606137091638706705333) | -| RTSTRUCT | RT STRUCT | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0) | -| 4D | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) | -| VIDEO | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) | -| microscopy | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) | -| ECG | ECG Waveform | [Demo](https://viewer-dev.ohif.org/viewer?StudyInstanceUIDs=2.25.209974489360710696739324151261716440238) | +--- ## About -The OHIF Viewer can retrieve -and load images from most sources and formats, render sets in 2D, 3D, and -reconstructed representations; allows for the manipulation, annotation, and -serialization of observations; supports internationalization, OpenID Connect, -offline use, hotkeys, and many more features. +This project is a **fork of the [OHIF Viewers](https://github.com/OHIF/Viewers)** open-source platform, extended to serve as a specialized dental imaging SaaS application. All dental features are self-contained within a custom extension and mode to ensure zero impact on the original OHIF behavior. -Almost everything offers some degree of customization and configuration. If it -doesn't support something you need, we accept pull requests and have an ever -improving Extension System. +Full OHIF documentation is available at [docs.ohif.org](https://docs.ohif.org/). -## Why Choose Us +--- -### Community & Experience +## Links -The OHIF Viewer is a collaborative effort that has served as the basis for many -active, production, and FDA Cleared medical imaging viewers. It benefits from -our extensive community's collective experience, and from the sponsored -contributions of individuals, research groups, and commercial organizations. +| Resource | URL | +|---|---| +| Live Website | [x7-dental.web.app](https://ohif-dental-viewer-962047575449.us-central1.run.app) | +| GitHub Repository | [github.com/bitbossing/24x7-ohif](https://github.com/bitbossing/24x7-ohif) | +| Demo Video | Coming soon | +| OHIF Official | [ohif.org](https://ohif.org/) | +| OHIF GitHub | [github.com/OHIF/Viewers](https://github.com/OHIF/Viewers) | +| OHIF Documentation | [docs.ohif.org](https://docs.ohif.org/) | +| OHIF Demo | [viewer.ohif.org](https://viewer.ohif.org/) | -### Built to Adapt +--- -After more than 8-years of integrating with many companies and organizations, -The OHIF Viewer has been rebuilt from the ground up to better address the -varying workflow and configuration needs of its many users. All of the Viewer's -core features are built using its own extension system. The same extensibility -that allows us to offer: +## General Rules -- 2D and 3D medical image viewing -- Multiplanar Reconstruction (MPR) -- Maximum Intensity Project (MIP) -- Whole slide microscopy viewing -- PDF and Dicom Structured Report rendering -- Segmentation rendering as labelmaps and contours -- User Access Control (UAC) -- Context specific toolbar and side panel content -- and many others +These rules were followed throughout the entire development process: -Can be leveraged by you to customize the viewer for your workflow, and to add -any new functionality you may need (and wish to maintain privately without -forking). +- **New actions & behaviors must only be visible in Dental Mode** β€” no pollution of the base OHIF experience +- **Must re-use original code UI style** β€” feels native, not bolted-on +- **Refrain from editing existing code files** except where required in Tasks C & D +- **Old behavior must not be affected** β€” only Dental Mode introduces changes +- **Push every item and open a Pull Request for every task** -### Support +--- -- [Report a Bug πŸ›](https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Community%3A+Report+%3Abug%3A%2CAwaiting+Reproduction&projects=&template=bug-report.yml&title=%5BBug%5D+) -- [Request a Feature πŸš€](https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Community%3A+Request+%3Ahand%3A&projects=&template=feature-request.yml&title=%5BFeature+Request%5D+) -- [Ask a Question πŸ€—](community.ohif.org) -- [Slack Channel](https://join.slack.com/t/cornerstonejs/shared_invite/zt-1r8xb2zau-dOxlD6jit3TN0Uwf928w9Q) +## Timeline -For commercial support, academic collaborations, and answers to common -questions; please use [Get Support](https://ohif.org/get-support/) to contact -us. +| Step | Time | Description | +|---|---|---| +| Step 1 | 0:30 | Understand Requirements & Breakdown Task | +| Step 2 | 1:00 | Test, Understand & Read OHIF Documentation | +| Step 3 | 2:00 | Task A β€” Setup Project & Dental Mode UI Customization | +| Step 4 | 2:00 | Task B β€” Dental Measurements Palette | +| Step 5 | 1:00 | Task C β€” User Preference Saving & Authentication | +| Step 6 | 1:00 | Task D β€” Refactor, Deployment & Testing | +| Step 7 | 0:30 | Documentation & Deliverables | +| **Total** | **8:00** | | -## Developing +--- -### Branches +## Tasks & Pull Requests -#### `master` branch - The latest dev (beta) release +### Task A β€” Setup Project & Dental Mode UI Customization +> PR: [bitbossing/24x7-ohif#1](https://github.com/bitbossing/24x7-ohif/pull/1) -- `master` - The latest dev release +- Fork the OHIF project +- Create and clone a branch to work on Task A +- Create a new mode `[dental-ui]` β€” "Dental Mode" based on Longitudinal Mode, isolated from original code +- Create a new extension `[dental-ui]` dedicated to Dental Mode, isolated from original code +- Add a **Theme Toggle button** on the Header to switch dental UI (color, font, icon) +- Add **Practice Name** after the Logo on the Header β€” `Dennis Jayvee Patricio` +- Auto-trigger **Patient Info** on the Header +- Add **Tooth Selector** on the Header +- Default to **2x2 Hanging Protocol** + - Top-left: Current image + - Top-right: Prior exam (empty if none) + - Bottom: Bitewing placeholders -This is typically where the latest development happens. Code that is in the master branch has passed code reviews and automated tests, but it may not be deemed ready for production. This branch usually contains the most recent changes and features being worked on by the development team. It's often the starting point for creating feature branches (where new features are developed) and hotfix branches (for urgent fixes). +--- -Each package is tagged with beta version numbers, and published to npm such as `@ohif/ui@3.6.0-beta.1` +### Task B β€” Dental Measurements Palette +> PR: [bitbossing/24x7-ohif#2](https://github.com/bitbossing/24x7-ohif/pull/2) -### `release/*` branches - The latest stable releases -Once the `master` branch code reaches a stable, release-ready state, we conduct a comprehensive code review and QA testing. Upon approval, we create a new release branch from `master`. These branches represent the latest stable version considered ready for production. +- Create and clone a branch to work on Task B +- Create a new set of **Measurement Tools** for Dental Mode, saved to a new Dental Measurements Panel with auto-incrementing labels per tool type: + - **PA Length** β€” Periapical Length Tool + - **Canal Angle** β€” Canal Angle Tool + - **Crown Width** β€” Crown Width Tool + - **Root Length** β€” Root Length Tool +- Add **Sort & Filter** to the Dental Measurements panel +- Add **Download button** to export measurements as JSON -For example, `release/3.5` is the branch for version 3.5.0, and `release/3.6` is for version 3.6.0. After each release, we wait a few days to ensure no critical bugs. If any are found, we fix them in the release branch and create a new release with a minor version bump, e.g., 3.5.1 in the `release/3.5` branch. +--- -Each package is tagged with version numbers and published to npm, such as `@ohif/ui@3.5.0`. Note that `master` is always ahead of the `release` branch. We publish docker builds for both beta and stable releases. +### Task C β€” User Preference Saving & Authentication +> PR: [bitbossing/24x7-ohif#3](https://github.com/bitbossing/24x7-ohif/pull/3) -Here is a schematic representation of our development workflow: +- Create and clone a branch to work on Task C +- Setup Firebase Project and create a **Google Sign-In Login Page** +- Create a **Logout button** (avatar dropdown in the global header) +- **Save user theme preference** to `localStorage` β€” persists across sessions and page refreshes -![alt text](platform/docs/docs/assets/img/github-readme-branches-Jun2024.png) +--- +### Task D β€” Refactor, Deployment & Testing +> PR: [bitbossing/24x7-ohif#4](https://github.com/bitbossing/24x7-ohif/pull/4) +- Create and clone a branch to work on Task D +- Build and deploy the project using **Docker β†’ Google Cloud Run β†’ Firebase Hosting** +- Full scale testing +- Fix discovered bugs +- Update README +--- +## File Changes -### Requirements +`[A]` Added   `[M]` Modified -- [Yarn 1.20.0+](https://yarnpkg.com/en/docs/install) -- [Node 18+](https://nodejs.org/en/) -- Yarn Workspaces should be enabled on your machine: - - `yarn config set workspaces-experimental true` +``` +β”œβ”€β”€ platform # +β”‚ β”œβ”€β”€ app # +β”‚ β”‚ β”œβ”€β”€ .env [M] # Firebase project keys +β”‚ β”‚ β”œβ”€β”€ package.json [M] # Added firebase dependency +β”‚ β”‚ └── src # +β”‚ β”‚ β”œβ”€β”€ App.tsx [M] # Firebase auth wrapper + UserInfo injection +β”‚ β”‚ β”œβ”€β”€ components # +β”‚ β”‚ β”‚ └── FirebaseUserInfo.tsx [A] # Avatar dropdown with logout +β”‚ β”‚ β”œβ”€β”€ routes # +β”‚ β”‚ β”‚ β”œβ”€β”€ LoginPage.tsx [A] # Google Sign-In page +β”‚ β”‚ β”‚ └── WorkList # +β”‚ β”‚ β”‚ └── WorkList.tsx [M] # Added UserInfo to worklist header +β”‚ β”‚ └── utils # +β”‚ β”‚ β”œβ”€β”€ firebaseConfig.ts [A] # Firebase app initialization +β”‚ β”‚ └── FirebaseAuthRoutes.tsx [A] # Auth gate (loading/login/app) +β”‚ └── ui-next # +β”‚ └── src/components/Header # +β”‚ └── Header.tsx [M] # Added UserInfo prop slot +β”œβ”€β”€ extensions # +β”‚ β”œβ”€β”€ default # +β”‚ β”‚ └── src/ViewerLayout # +β”‚ β”‚ └── ViewerHeader.tsx [M] # Reads UserInfo from customizationService +β”‚ └── 24x7-dental-ui # Dental Extension +β”‚ β”œβ”€β”€ package.json [M] # Removed firebase/ui-next peerDeps +β”‚ └── src # +β”‚ β”œβ”€β”€ index.tsx [M] # Registered toolbar modules, onModeExit reset +β”‚ β”œβ”€β”€ dentalThemeManager.ts [M] # Theme toggle + localStorage persistence +β”‚ β”œβ”€β”€ components # +β”‚ β”‚ β”œβ”€β”€ DentalBrandTitle.tsx [M] # "Dennis Jayvee Patricio" brand label +β”‚ β”‚ β”œβ”€β”€ DentalThemeToggleButton.tsx [M] # Theme toggle, restores on mount +β”‚ β”‚ β”œβ”€β”€ TabDentalMeasurements.tsx [A] # Measurements tab icon +β”‚ β”‚ └── ToothSelectorButton.tsx [A] # Tooth selector toolbar button +β”‚ β”œβ”€β”€ hangingProtocols # +β”‚ β”‚ └── hpDental2x2.ts [A] # 2x2 dental hanging protocol +β”‚ β”œβ”€β”€ layouts # +β”‚ β”‚ └── DentalViewerLayout.tsx [A] # Custom dental viewer layout +β”‚ β”œβ”€β”€ measurements # +β”‚ β”‚ └── registerDentalMappings.ts [A] # Dental measurement type mappings +β”‚ β”œβ”€β”€ panels # +β”‚ β”‚ β”œβ”€β”€ PanelDentalMeasurements.tsx [A] # Measurements panel with filters +β”‚ β”‚ └── PanelTrackedMeasurementsNoDental.tsx [A] # Fallback measurements panel +β”‚ β”œβ”€β”€ styles # +β”‚ β”‚ └── dental-theme.css [A] # Dental theme CSS overrides +β”‚ └── tools # +β”‚ β”œβ”€β”€ PALengthTool.ts [A] # Periapical length measurement tool +β”‚ β”œβ”€β”€ CanalAngleTool.ts [A] # Root canal angle tool +β”‚ β”œβ”€β”€ CrownWidthTool.ts [A] # Crown width measurement tool +β”‚ └── RootLengthTool.ts [A] # Root length measurement tool +β”œβ”€β”€ modes # +β”‚ └── 24x7-dental-ui # Dental Mode +β”‚ └── src # +β”‚ β”œβ”€β”€ index.tsx [M] # Mode config, toolbar layout, routes +β”‚ └── i18n/locales/en-US # +β”‚ └── Modes.json [A] # English translation strings +β”œβ”€β”€ Dockerfile [M] # Added Firebase build args +β”œβ”€β”€ README.md [M] # Added Firebase build args +β”œβ”€β”€ firebase.json [A] # Hosting β†’ Cloud Run proxy config +β”œβ”€β”€ .firebaserc [A] # Points CLI to x7-dental project +└── deploy.sh [A] # Build β†’ push β†’ Cloud Run β†’ Hosting +``` -### Getting Started +--- -1. [Fork this repository][how-to-fork] -2. [Clone your forked repository][how-to-clone] - - `git clone https://github.com/YOUR-USERNAME/Viewers.git` -3. Navigate to the cloned project's directory -4. Add this repo as a `remote` named `upstream` - - `git remote add upstream https://github.com/OHIF/Viewers.git` -5. `yarn install --frozen-lockfile` to restore dependencies and link projects +## Deployment -:::danger -In general run `yarn install` with the `--frozen-lockfile` flag to help avoid -supply chain attacks by enforcing reproducible dependencies. That is, if the -`yarn.lock` file is clean and does NOT reference compromised packages, then -no compromised packages should land on your machine by using this flag. -::: +### Prerequisites -#### To Develop +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) +- [Google Cloud CLI](https://cloud.google.com/sdk/docs/install) +- [Firebase CLI](https://firebase.google.com/docs/cli) β€” `npm install -g firebase-tools` +- [Git Bash](https://git-scm.com/) (Windows) -_From this repository's root directory:_ +### One-time Setup ```bash -# Enable Yarn Workspaces -yarn config set workspaces-experimental true +# 1. Authenticate with Google Cloud +gcloud init + +# 2. Enable required GCP APIs +gcloud services enable run.googleapis.com containerregistry.googleapis.com --project x7-dental + +# 3. Configure Docker to push to GCR +gcloud auth configure-docker + +# 4. Authenticate Firebase CLI +firebase login +``` + +Then activate **Firebase Hosting** in the [Firebase Console](https://console.firebase.google.com/) under your project. + +### Clone & Configure -# Restore dependencies +```bash +git clone https://github.com/bitbossing/24x7-ohif.git +cd 24x7-ohif yarn install --frozen-lockfile ``` -### Cornerstone3D Integration Testing - -OHIF's Playwright end-to-end tests can run against a **CS3D branch** or a -**published CS3D version**, allowing changes that span both repositories to be -validated together before merging. - -#### Setting up an integration build - -1. Add the **`ohif-integration`** label to your OHIF pull request. -2. In the PR body, add a line specifying the CS3D ref: - ``` - CS3D_REF: feat/my-feature - ``` - - **Version ref** (e.g. `4.19+`, `4.18.2`) β€” the workflow resolves it to an - exact published version and swaps the CS3D dependency via npm. - - **Branch ref** (e.g. `main`, `cornerstonejs:feat/foo`) β€” the workflow - clones the branch, builds CS3D from source with `bun run build:esm`, and - symlinks the built packages into OHIF's `node_modules`. - - For forks, use the `:` format - (e.g. `myGithubUser:feat/foo`). - - If no `CS3D_REF` is specified, the default is `4.19+`. -3. The workflow can also be triggered manually via **workflow_dispatch** with a - `cs3d_ref` input. - -#### What happens in CI - -The [Playwright workflow](.github/workflows/playwright.yml) runs two jobs: - -| Job | Purpose | -|-----|---------| -| **Playwright Tests** | Builds OHIF (with CS3D linked or version-swapped), runs the full Playwright suite, uploads test results and coverage, and deploys a Netlify preview when `ohif-integration` is active. | -| **CS3D Branch Merge Guard** | A lightweight check that **fails** when the `ohif-integration` label is present and `CS3D_REF` points to a branch (not a version). This prevents merging while still letting the Playwright tests show green so you can see whether the code actually works. | - -#### Testing changes that span both repos - -If a feature requires changes in both Cornerstone3D and OHIF: - -1. Create your feature branch in CS3D and push it. -2. Create a matching branch in OHIF. -3. Add the `ohif-integration` label to the OHIF pull request. -4. In the PR body, add: `CS3D_REF: `. -5. Playwright tests will build CS3D from source, link it, and run the full - suite. The merge guard will block merge until you switch to a published - version β€” but you can see the test results and the preview deploy while - iterating. -6. Once the CS3D side is merged and published, update the PR body to reference - the published version (e.g. `CS3D_REF: 4.19+`). The tests will run against - the registry version and the merge guard will pass. - -#### Preview deploys - -When `ohif-integration` is active, the Playwright workflow also builds the OHIF -viewer and deploys it to Netlify as a preview. This gives you a live URL to -manually test the combined CS3D + OHIF changes without running anything locally. - -For details on linking CS3D locally for development, see the -[Cornerstone3D README](libs/@cornerstonejs/README.md#local-development-linking--unlinking). - -## Commands - -These commands are available from the root directory. Each project directory -also supports a number of commands that can be found in their respective -`README.md` and `package.json` files. - -| Yarn Commands | Description | -| ---------------------------- | ------------------------------------------------------------- | -| **Develop** | | -| `dev` | Default development experience for Viewer | -| `dev:fast` | Our experimental fast dev mode that uses rsbuild instead of webpack | -| `test:unit` | Jest multi-project test runner; overall coverage | -| **Deploy** | | -| `build`\* | Builds production output for our PWA Viewer | | - -\* - For more information on different builds, check out our [Deploy -Docs][deployment-docs] - -## Project - -The OHIF Medical Image Viewing Platform is maintained as a -[`monorepo`][monorepo]. This means that this repository, instead of containing a -single project, contains many projects. If you explore our project structure, -you'll see the following: +Create `platform/app/.env` and fill in your Firebase project values: + +```env +REACT_APP_FIREBASE_API_KEY=your_key +REACT_APP_FIREBASE_AUTH_DOMAIN=your_project.firebaseapp.com +REACT_APP_FIREBASE_PROJECT_ID=your_project +REACT_APP_FIREBASE_STORAGE_BUCKET=your_project.firebasestorage.app +REACT_APP_FIREBASE_MESSAGING_SENDER_ID=your_sender_id +REACT_APP_FIREBASE_APP_ID=your_app_id +``` + +### Deploy + +Open **Git Bash**, navigate to the project root, and run: + +```bash +bash deploy.sh +``` + +This will: +1. Build the Docker image with Firebase keys baked in +2. Push to Google Container Registry +3. Deploy the container to Cloud Run +4. Deploy Firebase Hosting proxy rules + +Your app will be live at `https://your-project-id.web.app`. + +For subsequent deploys after code changes: ```bash -. -β”œβ”€β”€ extensions # -β”‚ β”œβ”€β”€ _example # Skeleton of example extension -β”‚ β”œβ”€β”€ default # basic set of useful functionalities (datasources, panels, etc) -β”‚ β”œβ”€β”€ cornerstone # image rendering and tools w/ Cornerstone3D -β”‚ β”œβ”€β”€ cornerstone-dicom-sr # DICOM Structured Report rendering and export -β”‚ β”œβ”€β”€ cornerstone-dicom-sr # DICOM Structured Report rendering and export -β”‚ β”œβ”€β”€ cornerstone-dicom-seg # DICOM Segmentation rendering and export -β”‚ β”œβ”€β”€ cornerstone-dicom-rt # DICOM RTSTRUCT rendering -β”‚ β”œβ”€β”€ cornerstone-microscopy # Whole Slide Microscopy rendering -β”‚ β”œβ”€β”€ dicom-pdf # PDF rendering -β”‚ β”œβ”€β”€ dicom-video # DICOM RESTful Services -β”‚ β”œβ”€β”€ measurement-tracking # Longitudinal measurement tracking -β”‚ β”œβ”€β”€ tmtv # Total Metabolic Tumor Volume (TMTV) calculation -| - -β”‚ -β”œβ”€β”€ modes # -β”‚ β”œβ”€β”€ _example # Skeleton of example mode -β”‚ β”œβ”€β”€ basic-dev-mode # Basic development mode -β”‚ β”œβ”€β”€ longitudinal # Longitudinal mode (measurement tracking) -β”‚ β”œβ”€β”€ tmtv # Total Metabolic Tumor Volume (TMTV) calculation mode -β”‚ └── microscopy # Whole Slide Microscopy mode -β”‚ -β”œβ”€β”€ platform # -β”‚ β”œβ”€β”€ core # Business Logic -β”‚ β”œβ”€β”€ i18n # Internationalization Support -β”‚ β”œβ”€β”€ ui # React component library -β”‚ β”œβ”€β”€ docs # Documentation -β”‚ └── viewer # Connects platform and extension projects -β”‚ -β”œβ”€β”€ ... # misc. shared configuration -β”œβ”€β”€ lerna.json # MonoRepo (Lerna) settings -β”œβ”€β”€ package.json # Shared devDependencies and commands -└── README.md # This file +bash deploy.sh # full rebuild + redeploy +bash deploy.sh --skip-build # redeploy existing image only ``` -## Acknowledgments - -To acknowledge the OHIF Viewer in an academic publication, please cite - -> _Open Health Imaging Foundation Viewer: An Extensible Open-Source Framework -> for Building Web-Based Imaging Applications to Support Cancer Research_ -> -> Erik Ziegler, Trinity Urban, Danny Brown, James Petts, Steve D. Pieper, Rob -> Lewis, Chris Hafey, and Gordon J. Harris -> -> _JCO Clinical Cancer Informatics_, no. 4 (2020), 336-345, DOI: -> [10.1200/CCI.19.00131](https://www.doi.org/10.1200/CCI.19.00131) -> -> Open-Access on Pubmed Central: -> https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7259879/ - -or, for v1, please cite: - -> _LesionTracker: Extensible Open-Source Zero-Footprint Web Viewer for Cancer -> Imaging Research and Clinical Trials_ -> -> Trinity Urban, Erik Ziegler, Rob Lewis, Chris Hafey, Cheryl Sadow, Annick D. -> Van den Abbeele and Gordon J. Harris -> -> _Cancer Research_, November 1 2017 (77) (21) e119-e122 DOI: -> [10.1158/0008-5472.CAN-17-0334](https://www.doi.org/10.1158/0008-5472.CAN-17-0334) - -**Note:** If you use or find this repository helpful, please take the time to -star this repository on GitHub. This is an easy way for us to assess adoption -and it can help us obtain future funding for the project. - -This work is supported primarily by the National Institutes of Health, National -Cancer Institute, Informatics Technology for Cancer Research (ITCR) program, -under a -[grant to Dr. Gordon Harris at Massachusetts General Hospital (U24 CA199460)](https://projectreporter.nih.gov/project_info_description.cfm?aid=8971104). - -[NCI Imaging Data Commons (IDC) project](https://imaging.datacommons.cancer.gov/) supported the development of new features and bug fixes marked with ["IDC:priority"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Apriority), -["IDC:candidate"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Acandidate) or ["IDC:collaboration"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Acollaboration). NCI Imaging Data Commons is supported by contract number 19X037Q from -Leidos Biomedical Research under Task Order HHSN26100071 from NCI. [IDC Viewer](https://learn.canceridc.dev/portal/visualization) is a customized version of the OHIF Viewer. - -This project is tested with BrowserStack. Thank you for supporting open-source! - -## License - -MIT Β© [OHIF](https://github.com/OHIF) - - - - - -[lerna-image]: https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg -[lerna-url]: https://lerna.js.org/ -[netlify-image]: https://api.netlify.com/api/v1/badges/32708787-c9b0-4634-b50f-7ca41952da77/deploy-status -[netlify-url]: https://app.netlify.com/sites/ohif-dev/deploys -[all-contributors-image]: https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square -[circleci-image]: https://circleci.com/gh/OHIF/Viewers.svg?style=svg -[circleci-url]: https://circleci.com/gh/OHIF/Viewers -[codecov-image]: https://codecov.io/gh/OHIF/Viewers/branch/master/graph/badge.svg -[codecov-url]: https://codecov.io/gh/OHIF/Viewers/branch/master -[prettier-image]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square -[prettier-url]: https://github.com/prettier/prettier -[semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg -[semantic-url]: https://github.com/semantic-release/semantic-release - -[npm-url]: https://npmjs.org/package/@ohif/app -[npm-downloads-image]: https://img.shields.io/npm/dm/@ohif/app.svg?style=flat-square -[npm-version-image]: https://img.shields.io/npm/v/@ohif/app.svg?style=flat-square -[docker-pulls-img]: https://img.shields.io/docker/pulls/ohif/viewer.svg?style=flat-square -[docker-image-url]: https://hub.docker.com/r/ohif/app -[license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square -[license-url]: LICENSE -[percy-image]: https://percy.io/static/images/percy-badge.svg -[percy-url]: https://percy.io/Open-Health-Imaging-Foundation/OHIF-Viewer - -[monorepo]: https://en.wikipedia.org/wiki/Monorepo -[how-to-fork]: https://help.github.com/en/articles/fork-a-repo -[how-to-clone]: https://help.github.com/en/articles/fork-a-repo#step-2-create-a-local-clone-of-your-fork -[ohif-architecture]: https://docs.ohif.org/architecture/index.html -[ohif-extensions]: https://docs.ohif.org/architecture/index.html -[deployment-docs]: https://docs.ohif.org/deployment/ -[react-url]: https://reactjs.org/ -[pwa-url]: https://developers.google.com/web/progressive-web-apps/ -[ohif-viewer-url]: https://www.npmjs.com/package/@ohif/app -[configuration-url]: https://docs.ohif.org/configuring/ -[extensions-url]: https://docs.ohif.org/extensions/ - -[platform-core]: platform/core/README.md -[core-npm]: https://www.npmjs.com/package/@ohif/core -[platform-i18n]: platform/i18n/README.md -[i18n-npm]: https://www.npmjs.com/package/@ohif/i18n -[platform-ui]: platform/ui/README.md -[ui-npm]: https://www.npmjs.com/package/@ohif/ui -[platform-viewer]: platform/app/README.md -[viewer-npm]: https://www.npmjs.com/package/@ohif/app - -[extension-cornerstone]: extensions/cornerstone/README.md -[cornerstone-npm]: https://www.npmjs.com/package/@ohif/extension-cornerstone -[extension-dicom-html]: extensions/dicom-html/README.md -[html-npm]: https://www.npmjs.com/package/@ohif/extension-dicom-html -[extension-dicom-microscopy]: extensions/dicom-microscopy/README.md -[microscopy-npm]: https://www.npmjs.com/package/@ohif/extension-dicom-microscopy -[extension-dicom-pdf]: extensions/dicom-pdf/README.md -[pdf-npm]: https://www.npmjs.com/package/@ohif/extension-dicom-pdf -[extension-vtk]: extensions/vtk/README.md -[vtk-npm]: https://www.npmjs.com/package/@ohif/extension-vtk - - -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FOHIF%2FViewers.svg?type=large&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large&issueType=license) +--- + +## Credits + +This project is built on top of the **OHIF Medical Imaging Viewer**, an open-source project by the [Open Health Imaging Foundation](https://ohif.org/). All original rights and licenses belong to the OHIF contributors. + +- OHIF Website: [ohif.org](https://ohif.org/) +- OHIF GitHub: [github.com/OHIF/Viewers](https://github.com/OHIF/Viewers) +- OHIF Documentation: [docs.ohif.org](https://docs.ohif.org/) +- OHIF License: [MIT](https://github.com/OHIF/Viewers/blob/master/LICENSE) + +--- + +
+ Built by Dennis Jayvee Patricio  Β·  April 13, 2026 +
diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 00000000000..d3b64bb6154 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# ============================================================================= +# deploy.sh β€” Build β†’ Push β†’ Deploy to Cloud Run + Firebase Hosting +# +# Usage: +# ./deploy.sh # full deploy (build + push + cloud run + hosting) +# ./deploy.sh --skip-build # redeploy existing image without rebuilding +# ============================================================================= + +set -euo pipefail + +# ---------- Windows: point gcloud at its bundled Python ---------- +# On Windows, the system `python` is a Store alias stub; gcloud needs a real interpreter. +if [[ -n "${LOCALAPPDATA:-}" ]]; then + _BUNDLED_PY="${LOCALAPPDATA}/Google/Cloud SDK/google-cloud-sdk/platform/bundledpython/python.exe" + if [[ -f "$_BUNDLED_PY" ]]; then + export CLOUDSDK_PYTHON="$_BUNDLED_PY" + fi +fi + +# ---------- config (edit if needed) ---------- +GCP_PROJECT="x7-dental" +GCP_REGION="us-central1" +SERVICE_NAME="ohif-dental-viewer" +IMAGE="gcr.io/${GCP_PROJECT}/${SERVICE_NAME}" + +# Load Firebase env vars from platform/app/.env +ENV_FILE="platform/app/.env" +if [[ ! -f "$ENV_FILE" ]]; then + echo "ERROR: $ENV_FILE not found" + exit 1 +fi +source <(grep "^REACT_APP_FIREBASE" "$ENV_FILE" | sed 's/ *= */=/') + +# ---------- build & push ---------- +if [[ "${1:-}" != "--skip-build" ]]; then + echo "==> Building Docker image..." + docker build \ + --build-arg REACT_APP_FIREBASE_API_KEY="$REACT_APP_FIREBASE_API_KEY" \ + --build-arg REACT_APP_FIREBASE_AUTH_DOMAIN="$REACT_APP_FIREBASE_AUTH_DOMAIN" \ + --build-arg REACT_APP_FIREBASE_PROJECT_ID="$REACT_APP_FIREBASE_PROJECT_ID" \ + --build-arg REACT_APP_FIREBASE_STORAGE_BUCKET="$REACT_APP_FIREBASE_STORAGE_BUCKET" \ + --build-arg REACT_APP_FIREBASE_MESSAGING_SENDER_ID="$REACT_APP_FIREBASE_MESSAGING_SENDER_ID" \ + --build-arg REACT_APP_FIREBASE_APP_ID="$REACT_APP_FIREBASE_APP_ID" \ + -t "$IMAGE" \ + . + + echo "==> Pushing image to Google Container Registry..." + docker push "$IMAGE" +fi + +# ---------- deploy to Cloud Run ---------- +echo "==> Deploying to Cloud Run ($GCP_REGION)..." +gcloud run deploy "$SERVICE_NAME" \ + --image "$IMAGE" \ + --platform managed \ + --region "$GCP_REGION" \ + --project "$GCP_PROJECT" \ + --port 8080 \ + --allow-unauthenticated \ + --min-instances 0 \ + --max-instances 3 \ + --memory 512Mi \ + --cpu 1 + +# ---------- deploy Firebase Hosting ---------- +echo "==> Deploying Firebase Hosting rules..." +firebase deploy --only hosting --project "$GCP_PROJECT" + +echo "" +echo "Done! Your app is live at:" +echo " https://${GCP_PROJECT}.web.app" diff --git a/extensions/24x7-dental-ui/.gitignore b/extensions/24x7-dental-ui/.gitignore new file mode 100644 index 00000000000..67045665db2 --- /dev/null +++ b/extensions/24x7-dental-ui/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/extensions/24x7-dental-ui/.prettierrc b/extensions/24x7-dental-ui/.prettierrc new file mode 100644 index 00000000000..ef83baaef93 --- /dev/null +++ b/extensions/24x7-dental-ui/.prettierrc @@ -0,0 +1,11 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"], + "trailingComma": "es5", + "printWidth": 100, + "proseWrap": "always", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/extensions/24x7-dental-ui/.webpack/webpack.prod.js b/extensions/24x7-dental-ui/.webpack/webpack.prod.js new file mode 100644 index 00000000000..3a78c481305 --- /dev/null +++ b/extensions/24x7-dental-ui/.webpack/webpack.prod.js @@ -0,0 +1,96 @@ +const path = require('path'); +const pkg = require('../package.json'); + +const outputFile = 'index.umd.js'; +const rootDir = path.resolve(__dirname, '../'); +const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`); + +// Todo: add ESM build for the extension in addition to umd build + +const config = { + mode: 'production', + entry: rootDir + '/' + pkg.module, + devtool: 'source-map', + output: { + path: outputFolder, + filename: outputFile, + library: pkg.name, + libraryTarget: 'umd', + chunkFilename: '[name].chunk.js', + umdNamedDefine: true, + globalObject: "typeof self !== 'undefined' ? self : this", + }, + externals: [ + { + react: { + root: 'React', + commonjs2: 'react', + commonjs: 'react', + amd: 'react', + }, + '@ohif/core': { + commonjs2: '@ohif/core', + commonjs: '@ohif/core', + amd: '@ohif/core', + root: '@ohif/core', + }, + '@ohif/ui': { + commonjs2: '@ohif/ui', + commonjs: '@ohif/ui', + amd: '@ohif/ui', + root: '@ohif/ui', + }, + }, + ], + module: { + + rules: [ + { + test: /\.svg?$/, + oneOf: [ + { + use: [ + { + loader: '@svgr/webpack', + options: { + svgoConfig: { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false + }, + }, + }, + ] + }, + prettier: false, + svgo: true, + titleProp: true, + }, + }, + ], + issuer: { + and: [/\.(ts|tsx|js|jsx|md|mdx)$/], + }, + }, + ], + }, + { + test: /(\.jsx|\.js|\.tsx|\.ts)$/, + loader: 'babel-loader', + exclude: /(node_modules|bower_components)/, + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, + ], + }, + resolve: { + modules: [path.resolve('./node_modules'), path.resolve('./src')], + extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'], + }, +}; + +module.exports = config; diff --git a/extensions/24x7-dental-ui/LICENSE b/extensions/24x7-dental-ui/LICENSE new file mode 100644 index 00000000000..102957d12de --- /dev/null +++ b/extensions/24x7-dental-ui/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2026 24x7-dental-ui (dennis.jayvee.patricio.03@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/extensions/24x7-dental-ui/README.md b/extensions/24x7-dental-ui/README.md new file mode 100644 index 00000000000..b5f621ff2f7 --- /dev/null +++ b/extensions/24x7-dental-ui/README.md @@ -0,0 +1,7 @@ +# 24x7-dental-ui +## Description +24x7 Extension for Dental SaaS UI customization for OHIF Viewer +## Author +Dennis Jayvee Patricio +## License +MIT \ No newline at end of file diff --git a/extensions/24x7-dental-ui/babel.config.js b/extensions/24x7-dental-ui/babel.config.js new file mode 100644 index 00000000000..9fd3d5d30fd --- /dev/null +++ b/extensions/24x7-dental-ui/babel.config.js @@ -0,0 +1,49 @@ +module.exports = { + plugins: [ + ['@babel/plugin-transform-class-properties', { loose: true }], + '@babel/plugin-transform-typescript', + ['@babel/plugin-transform-private-property-in-object', { loose: true }], + ['@babel/plugin-transform-private-methods', { loose: true }], + ], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + ], + '@babel/preset-react', + '@babel/preset-typescript', + ], + plugins: [ + '@babel/plugin-transform-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + '@babel/plugin-transform-typescript', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/extensions/24x7-dental-ui/package.json b/extensions/24x7-dental-ui/package.json new file mode 100644 index 00000000000..f70e8289cfd --- /dev/null +++ b/extensions/24x7-dental-ui/package.json @@ -0,0 +1,71 @@ +{ + "name": "@ohif/extension-24x7-dental-ui", + "version": "1.0.0", + "description": "24x7 Extension for Dental SaaS UI customization for OHIF Viewer", + "author": "Dennis Jayvee Patricio", + "license": "MIT", + "main": "src/index.tsx", + "files": [ + "dist/**", + "public/**", + "README.md" + ], + "repository": "OHIF/Viewers", + "keywords": [ + "ohif-extension" + ], + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.18.0" + }, + "scripts": { + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:my-extension": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev" + }, + "peerDependencies": { + "@ohif/core": "^3.13.0-beta.56", + "@ohif/extension-default": "^3.13.0-beta.56", + "@ohif/extension-cornerstone": "^3.13.0-beta.56", + "@ohif/i18n": "^1.0.0", + "prop-types": "^15.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^12.2.2", + "react-router": "^6.23.1", + "react-router-dom": "^6.23.1", + "webpack": "5.89.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "@babel/runtime": "^7.20.13" + }, + "devDependencies": { + "@babel/core": "7.28.0", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-regenerator": "^7.16.7", + "@babel/plugin-transform-runtime": "7.28.0", + "@babel/plugin-transform-typescript": "^7.28.0", + "@babel/preset-env": "7.28.0", + "@babel/preset-react": "^7.27.1", + "@babel/preset-typescript": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "babel-loader": "^8.2.4", + "@svgr/webpack": "^8.1.0", + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^10.2.0", + "cross-env": "^7.0.3", + "dotenv": "^14.1.0", + "webpack": "5.89.0", + "webpack-merge": "^5.7.3", + "webpack-cli": "^5.0.2" + } +} diff --git a/extensions/24x7-dental-ui/src/components/DentalBrandTitle.tsx b/extensions/24x7-dental-ui/src/components/DentalBrandTitle.tsx new file mode 100644 index 00000000000..9495af9c1ed --- /dev/null +++ b/extensions/24x7-dental-ui/src/components/DentalBrandTitle.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export default function DentalBrandTitle(): React.ReactElement { + return ( +
+ + Dennis Jayvee Patricio + +
+ ); +} diff --git a/extensions/24x7-dental-ui/src/components/DentalThemeToggleButton.tsx b/extensions/24x7-dental-ui/src/components/DentalThemeToggleButton.tsx new file mode 100644 index 00000000000..e5060e4b787 --- /dev/null +++ b/extensions/24x7-dental-ui/src/components/DentalThemeToggleButton.tsx @@ -0,0 +1,56 @@ +import React, { useEffect, useState } from 'react'; +import { ToolButton } from '@ohif/ui-next'; +import { dentalThemeManager } from '../dentalThemeManager'; + +function ComputerThemeIcon({ active }: { active: boolean }): React.ReactElement { + return ( + + ); +} + +export default function DentalThemeToggleButton(): React.ReactElement { + const [isDentalTheme, setIsDentalTheme] = useState(() => dentalThemeManager.isActive()); + + useEffect(() => { + dentalThemeManager.restore(); + setIsDentalTheme(dentalThemeManager.isActive()); + + const unsubscribe = dentalThemeManager.subscribe(() => { + setIsDentalTheme(dentalThemeManager.isActive()); + }); + return unsubscribe; + }, []); + + return ( + dentalThemeManager.setActive(!isDentalTheme)} + > + {isDentalTheme! ? : } + + ); +} diff --git a/extensions/24x7-dental-ui/src/components/TabDentalMeasurements.tsx b/extensions/24x7-dental-ui/src/components/TabDentalMeasurements.tsx new file mode 100644 index 00000000000..3efac090d25 --- /dev/null +++ b/extensions/24x7-dental-ui/src/components/TabDentalMeasurements.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +const TabDentalMeasurements = (props: React.SVGProps) => ( + + + + + + +); + +export default TabDentalMeasurements; diff --git a/extensions/24x7-dental-ui/src/components/ToothSelectorButton.tsx b/extensions/24x7-dental-ui/src/components/ToothSelectorButton.tsx new file mode 100644 index 00000000000..c98f545525b --- /dev/null +++ b/extensions/24x7-dental-ui/src/components/ToothSelectorButton.tsx @@ -0,0 +1,308 @@ +import React, { useState, useCallback } from 'react'; +import { + Button, + Popover, + PopoverContent, + PopoverTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, + cn, +} from '@ohif/ui-next'; + +type NamingSystem = 'FDI' | 'Universal'; +const UPPER_ROW: readonly number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; +const LOWER_ROW: readonly number[] = [ + 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, +]; +const UNIVERSAL_TO_FDI: Readonly> = { + 1: 18, + 2: 17, + 3: 16, + 4: 15, + 5: 14, + 6: 13, + 7: 12, + 8: 11, + 9: 21, + 10: 22, + 11: 23, + 12: 24, + 13: 25, + 14: 26, + 15: 27, + 16: 28, + 17: 38, + 18: 37, + 19: 36, + 20: 35, + 21: 34, + 22: 33, + 23: 32, + 24: 31, + 25: 41, + 26: 42, + 27: 43, + 28: 44, + 29: 45, + 30: 46, + 31: 47, + 32: 48, +}; + +const BTN_BASE = '!rounded-lg inline-flex items-center justify-center w-10 h-10'; +const BTN_DEFAULT = 'bg-transparent text-foreground/80 hover:bg-background hover:text-highlight'; +const BTN_ACTIVE = 'bg-highlight text-background hover:!bg-highlight/80'; + +function toDisplay(universalId: number, system: NamingSystem): number { + return system === 'FDI' ? UNIVERSAL_TO_FDI[universalId] : universalId; +} + +function formatSelectedLabel(selected: Set, system: NamingSystem): string { + if (selected.size === 0) return ''; + return [...selected] + .sort((a, b) => toDisplay(a, system) - toDisplay(b, system)) + .map(n => toDisplay(n, system)) + .join(', '); +} + +interface ToothCellProps { + universalId: number; + system: NamingSystem; + isSelected: boolean; + onToggle: (id: number) => void; +} + +function ToothCell({ + universalId, + system, + isSelected, + onToggle, +}: ToothCellProps): React.ReactElement { + const displayNum = toDisplay(universalId, system); + return ( + + ); +} + +interface ToothArchRowProps { + teeth: readonly number[]; + selected: Set; + system: NamingSystem; + onToggle: (id: number) => void; +} + +function ToothArchRow({ + teeth, + selected, + system, + onToggle, +}: ToothArchRowProps): React.ReactElement { + const leftQuadrant = teeth.slice(0, 8); + const rightQuadrant = teeth.slice(8); + return ( +
+ {leftQuadrant.map(id => ( + + ))} + {/* Midline separator */} +
+ {rightQuadrant.map(id => ( + + ))} +
+ ); +} + +function ToothIcon(): React.ReactElement { + return ( + + ); +} + +export default function ToothSelectorButton(): React.ReactElement { + const [open, setOpen] = useState(false); + const [selectedTeeth, setSelectedTeeth] = useState>(new Set()); + const [namingSystem, setNamingSystem] = useState('FDI'); + + const isActive = open || selectedTeeth.size > 0; + + const toggleTooth = useCallback((id: number): void => { + setSelectedTeeth(prev => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }, []); + + const clearSelection = useCallback((): void => { + setSelectedTeeth(new Set()); + }, []); + + return ( + + + + + + + + + +
+
Tooth Selector
+
+ Select teeth by FDI or Universal number +
+
+
+ + +
+ + Tooth Selector + + +
+ {(['FDI', 'Universal'] as const).map(sys => ( + + ))} +
+
+ +
+
+ + Upper Right (R) + + + + (L) Upper Left + +
+ + + + + +
+ + Lower Right (R) + + + + (L) Lower Left + +
+
+ +
+ + {selectedTeeth.size > 0 ? ( + <> + {selectedTeeth.size} + {` selected (${namingSystem}): `} + {formatSelectedLabel(selectedTeeth, namingSystem)} + + ) : ( + No teeth selected + )} + + + {selectedTeeth.size > 0 && ( + + )} +
+
+
+
+ ); +} diff --git a/extensions/24x7-dental-ui/src/declarations.d.ts b/extensions/24x7-dental-ui/src/declarations.d.ts new file mode 100644 index 00000000000..fa9154c3783 --- /dev/null +++ b/extensions/24x7-dental-ui/src/declarations.d.ts @@ -0,0 +1,4 @@ +declare module '*.css' { + const content: Record; + export default content; +} diff --git a/extensions/24x7-dental-ui/src/dentalThemeManager.ts b/extensions/24x7-dental-ui/src/dentalThemeManager.ts new file mode 100644 index 00000000000..0ab5fa95c1c --- /dev/null +++ b/extensions/24x7-dental-ui/src/dentalThemeManager.ts @@ -0,0 +1,66 @@ +const DENTAL_THEME_CLASS = 'dental-theme' as const; +const STORAGE_KEY = 'dentalThemeActive'; + +type ThemeListener = () => void; + +const listeners = new Set(); + +function notifyListeners(): void { + listeners.forEach(fn => fn()); +} + +function subscribe(listener: ThemeListener): () => void { + listeners.add(listener); + return () => listeners.delete(listener); +} + +function isActive(): boolean { + return document.documentElement.classList.contains(DENTAL_THEME_CLASS); +} + +function setActive(active: boolean): void { + if (active === isActive()) return; + + if (active) { + document.documentElement.classList.add(DENTAL_THEME_CLASS); + } else { + document.documentElement.classList.remove(DENTAL_THEME_CLASS); + } + + try { + localStorage.setItem(STORAGE_KEY, String(active)); + } catch { + // storage unavailable β€” ignore + } + + notifyListeners(); +} + +function restore(): void { + try { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved === 'true') { + document.documentElement.classList.add(DENTAL_THEME_CLASS); + } + } catch { + // storage unavailable β€” ignore + } +} + +function reset(): void { + if (isActive()) { + document.documentElement.classList.remove(DENTAL_THEME_CLASS); + notifyListeners(); + } +} + +restore(); + +export const dentalThemeManager = { + DENTAL_THEME_CLASS, + isActive, + setActive, + subscribe, + reset, + restore, +} as const; diff --git a/extensions/24x7-dental-ui/src/hangingProtocols/hpDental2x2.ts b/extensions/24x7-dental-ui/src/hangingProtocols/hpDental2x2.ts new file mode 100644 index 00000000000..705d3fb148e --- /dev/null +++ b/extensions/24x7-dental-ui/src/hangingProtocols/hpDental2x2.ts @@ -0,0 +1,160 @@ +import { Types } from '@ohif/core'; + +export const DENTAL_HP_2X2_ID = '@24x7-dental-ui/hp2x2'; + +const currentStudyMatchingRules: Types.HangingProtocol.MatchingRule[] = [ + { + attribute: 'studyInstanceUIDsIndex', + from: 'options', + required: true, + constraint: { equals: { value: 0 } }, + }, +]; + +const priorStudyMatchingRules: Types.HangingProtocol.MatchingRule[] = [ + { + attribute: 'studyInstanceUIDsIndex', + from: 'options', + required: true, + constraint: { equals: { value: 1 } }, + }, +]; + +const seriesWithImages: Types.HangingProtocol.MatchingRule[] = [ + { + attribute: 'numImageFrames', + weight: 1, + required: true, + constraint: { greaterThan: { value: 0 } }, + }, + // Prefer series explicitly referenced in the URL (e.g., deep-link to a study). + { + attribute: 'isDisplaySetFromUrl', + weight: 20, + constraint: { equals: true }, + }, +]; + +const biteWingIdentificationRule: Types.HangingProtocol.MatchingRule = { + attribute: 'SeriesDescription', + weight: 100, + required: true, + constraint: { + containsI: ['bitewing', 'bite wing', 'interproximal', 'bwx', 'bw xray', 'intraoral bw'], + }, +}; + +const stackViewportOptions: Types.HangingProtocol.ViewportOptions = { + viewportType: 'stack', + toolGroupId: 'default', + allowUnmatchedView: true, +}; + +const hpDental2x2: Types.HangingProtocol.Protocol = { + id: DENTAL_HP_2X2_ID, + name: 'Dental 2Γ—2', + description: + 'Dental view: current image (top-left), prior exam (top-right), bitewing placeholders (bottom row).', + numberOfPriorsReferenced: 1, + protocolMatchingRules: [ + { + id: 'hasImages', + weight: 25, + attribute: 'numberOfDisplaySetsWithImages', + constraint: { greaterThan: 0 }, + }, + ], + toolGroupIds: ['default'], + displaySetSelectors: { + currentSeriesId: { + studyMatchingRules: currentStudyMatchingRules, + seriesMatchingRules: seriesWithImages, + }, + priorSeriesId: { + allowUnmatchedView: true, + studyMatchingRules: priorStudyMatchingRules, + seriesMatchingRules: [ + { + attribute: 'numImageFrames', + weight: 1, + constraint: { greaterThan: { value: 0 } }, + }, + ], + }, + biteWingSeriesId: { + allowUnmatchedView: true, + studyMatchingRules: currentStudyMatchingRules, + seriesMatchingRules: [ + ...seriesWithImages.filter(r => r.attribute === 'numImageFrames'), + biteWingIdentificationRule, + ], + }, + }, + defaultViewport: { + viewportOptions: stackViewportOptions, + displaySets: [ + { + id: 'currentSeriesId', + matchedDisplaySetsIndex: -1, + }, + ], + }, + stages: [ + { + id: 'dental-2x2', + name: '2Γ—2', + stageActivation: { + enabled: { + minViewportsMatched: 1, + }, + }, + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 2, + columns: 2, + }, + }, + viewports: [ + { + viewportOptions: stackViewportOptions, + displaySets: [ + { + id: 'currentSeriesId', + matchedDisplaySetsIndex: 0, + }, + ], + }, + { + viewportOptions: stackViewportOptions, + displaySets: [ + { + id: 'priorSeriesId', + matchedDisplaySetsIndex: 0, + }, + ], + }, + { + viewportOptions: stackViewportOptions, + displaySets: [ + { + id: 'biteWingSeriesId', + matchedDisplaySetsIndex: 0, + }, + ], + }, + { + viewportOptions: stackViewportOptions, + displaySets: [ + { + id: 'biteWingSeriesId', + matchedDisplaySetsIndex: 1, + }, + ], + }, + ], + }, + ], +}; + +export default hpDental2x2; diff --git a/extensions/24x7-dental-ui/src/id.js b/extensions/24x7-dental-ui/src/id.js new file mode 100644 index 00000000000..ebe5acd98ae --- /dev/null +++ b/extensions/24x7-dental-ui/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/extensions/24x7-dental-ui/src/index.tsx b/extensions/24x7-dental-ui/src/index.tsx new file mode 100644 index 00000000000..28d7bc84688 --- /dev/null +++ b/extensions/24x7-dental-ui/src/index.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { id } from './id'; +import DentalBrandTitle from './components/DentalBrandTitle'; +import DentalThemeToggleButton from './components/DentalThemeToggleButton'; +import ToothSelectorButton from './components/ToothSelectorButton'; +import hpDental2x2 from './hangingProtocols/hpDental2x2'; +import { createDentalViewerLayout } from './layouts/DentalViewerLayout'; +import { dentalThemeManager } from './dentalThemeManager'; +import './styles/dental-theme.css'; +import { addTool } from '@cornerstonejs/tools'; +import { Icons } from '@ohif/ui-next'; +import TabDentalMeasurements from './components/TabDentalMeasurements'; +import PanelDentalMeasurements from './panels/PanelDentalMeasurements'; +import PanelTrackedMeasurementsNoDental from './panels/PanelTrackedMeasurementsNoDental'; +import PALengthTool from './tools/PALengthTool'; +import CanalAngleTool from './tools/CanalAngleTool'; +import CrownWidthTool from './tools/CrownWidthTool'; +import RootLengthTool from './tools/RootLengthTool'; + +export default { + id, + preRegistration() { + addTool(PALengthTool); + addTool(CanalAngleTool); + addTool(CrownWidthTool); + addTool(RootLengthTool); + Icons.addIcon('tab-dental-measurements', TabDentalMeasurements); + }, + onModeExit(): void { + dentalThemeManager.reset(); + }, + getToolbarModule(): Array<{ name: string; defaultComponent: React.ComponentType }> { + return [ + { + name: 'dental.themeToggle', + defaultComponent: DentalThemeToggleButton, + }, + { + name: 'dental.brandTitle', + defaultComponent: DentalBrandTitle, + }, + { + name: 'dental.toothSelector', + defaultComponent: ToothSelectorButton, + }, + ]; + }, + getPanelModule({ + commandsManager, + extensionManager, + servicesManager, + }: { + commandsManager: any; + extensionManager: any; + servicesManager: any; + }) { + return [ + { + name: 'dentalMeasurements', + iconName: 'tab-dental-measurements', + iconLabel: 'Dental', + label: 'Dental Measurements', + component: (props: any) => ( + + ), + }, + { + name: 'trackedMeasurementsNoDental', + iconName: 'tab-linear', + iconLabel: 'Measure', + label: 'Measurements', + component: (props: any) => ( + + ), + }, + ]; + }, + getViewportModule: () => [], + getLayoutTemplateModule({ extensionManager }: { extensionManager: any }) { + return [ + { + name: 'dentalViewerLayout', + id: 'dentalViewerLayout', + component: createDentalViewerLayout(extensionManager), + }, + ]; + }, + getSopClassHandlerModule: () => [], + getHangingProtocolModule: () => [ + { + name: hpDental2x2.id, + protocol: hpDental2x2, + }, + ], + getCommandsModule: () => [], + getContextModule: () => [], + getDataSourcesModule: () => [], +}; + +export { dentalThemeManager }; diff --git a/extensions/24x7-dental-ui/src/layouts/DentalViewerLayout.tsx b/extensions/24x7-dental-ui/src/layouts/DentalViewerLayout.tsx new file mode 100644 index 00000000000..86dc6d868e1 --- /dev/null +++ b/extensions/24x7-dental-ui/src/layouts/DentalViewerLayout.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { AppConfigProvider, useAppConfig } from '@state'; + +export function createDentalViewerLayout(extensionManager: any) { + return function DentalViewerLayout(props: any) { + const [outerAppConfig] = useAppConfig() || [{}, () => {}]; + const dentalAppConfig = { ...outerAppConfig, showPatientInfo: 'visible' }; + + const defaultEntry = extensionManager.getModuleEntry( + '@ohif/extension-default.layoutTemplateModule.viewerLayout' + ); + const DefaultLayout = defaultEntry?.component; + + if (!DefaultLayout) { + console.error( + '[24x7-dental-ui] Default ViewerLayout not found β€” check extension registration order.' + ); + return null; + } + + return ( + + + + ); + }; +} diff --git a/extensions/24x7-dental-ui/src/measurements/registerDentalMappings.ts b/extensions/24x7-dental-ui/src/measurements/registerDentalMappings.ts new file mode 100644 index 00000000000..68cf2400726 --- /dev/null +++ b/extensions/24x7-dental-ui/src/measurements/registerDentalMappings.ts @@ -0,0 +1,246 @@ +import { locking, visibility } from '@cornerstonejs/tools/annotation'; +import { utils, MeasurementService } from '@ohif/core'; +import { measurementMappingUtils } from '@ohif/extension-cornerstone'; + +const { getSOPInstanceAttributes } = measurementMappingUtils; +const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools'; +const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1'; +let _registered = false; + +function getLengthMappedAnnotations(annotation: any, displaySetService: any) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + if (!Object.keys(cachedStats).length) return []; + + const annotations: any[] = []; + Object.keys(cachedStats).forEach(() => { + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + const { SeriesNumber } = displaySet; + const stats = Object.values(cachedStats)[0] as any; + const { length, unit = 'mm' } = stats; + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + unit, + length, + }); + }); + return annotations; +} + +function getLengthDisplayText(mappedAnnotations: any[], displaySet: any) { + const displayText = { primary: [] as string[], secondary: [] as string[] }; + if (!mappedAnnotations?.length) return displayText; + + const { length, SeriesNumber, SOPInstanceUID, frameNumber, unit } = mappedAnnotations[0]; + if (length == null) return displayText; + + const instance = displaySet.instances?.find((img: any) => img.SOPInstanceUID === SOPInstanceUID); + const instanceText = instance?.InstanceNumber ? ` I: ${instance.InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + + displayText.primary.push(`${utils.roundNumber(length, 2)} ${unit}`); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + return displayText; +} + +function getAngleMappedAnnotations(annotation: any, displaySetService: any) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + if (!Object.keys(cachedStats).length) return []; + + const annotations: any[] = []; + Object.keys(cachedStats).forEach(() => { + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + const { SeriesNumber } = displaySet; + const stats = Object.values(cachedStats)[0] as any; + const angle = stats?.angle; + const unit = '\u00B0'; + annotations.push({ SeriesInstanceUID, SOPInstanceUID, SeriesNumber, frameNumber, unit, angle }); + }); + return annotations; +} + +function getAngleDisplayText(mappedAnnotations: any[], displaySet: any) { + const displayText = { primary: [] as string[], secondary: [] as string[] }; + if (!mappedAnnotations?.length) return displayText; + + const { angle, SeriesNumber, SOPInstanceUID, frameNumber, unit } = mappedAnnotations[0]; + if (angle == null) return displayText; + + const instance = displaySet.instances?.find((img: any) => img.SOPInstanceUID === SOPInstanceUID); + const instanceText = instance?.InstanceNumber ? ` I: ${instance.InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + + displayText.primary.push(`${utils.roundNumber(angle, 2)} ${unit}`); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + return displayText; +} + +function makeLengthToMeasurement(displaySetService: any) { + return (csToolsEventDetail: any) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + if (!metadata || !data) return null; + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const isLocked = locking.isAnnotationLocked(annotationUID); + const isVisible = visibility.isAnnotationVisible(annotationUID); + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = SOPInstanceUID + ? displaySetService.getDisplaySetForSOPInstanceUID(SOPInstanceUID, SeriesInstanceUID) + : displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { points, textBox } = data.handles; + const mappedAnnotations = getLengthMappedAnnotations(annotation, displaySetService); + const displayText = getLengthDisplayText(mappedAnnotations, displaySet); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + textBox, + isLocked, + isVisible, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + referencedImageId, + frameNumber: mappedAnnotations[0]?.frameNumber || 1, + toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText, + data: data.cachedStats, + type: MeasurementService.VALUE_TYPES.POLYLINE, + getReport: () => ({ + columns: ['AnnotationType', 'Length', 'Unit'], + values: [toolName, mappedAnnotations[0]?.length ?? '', mappedAnnotations[0]?.unit ?? 'mm'], + }), + }; + }; +} + +function makeAngleToMeasurement(displaySetService: any) { + return (csToolsEventDetail: any) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + if (!metadata || !data) return null; + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const isLocked = locking.isAnnotationLocked(annotationUID); + const isVisible = visibility.isAnnotationVisible(annotationUID); + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = SOPInstanceUID + ? displaySetService.getDisplaySetForSOPInstanceUID(SOPInstanceUID, SeriesInstanceUID) + : displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { points, textBox } = data.handles; + const mappedAnnotations = getAngleMappedAnnotations(annotation, displaySetService); + const displayText = getAngleDisplayText(mappedAnnotations, displaySet); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + textBox, + isLocked, + isVisible, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + referencedImageId, + frameNumber: mappedAnnotations[0]?.frameNumber || 1, + toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText, + data: data.cachedStats, + type: MeasurementService.VALUE_TYPES.ANGLE, + getReport: () => ({ + columns: ['AnnotationType', 'Angle (\u00B0)'], + values: [toolName, mappedAnnotations[0]?.angle ?? ''], + }), + }; + }; +} + +export function registerDentalMappings(measurementService: any, servicesManager: any): void { + if (_registered) return; + + const source = measurementService.getSource( + CORNERSTONE_3D_TOOLS_SOURCE_NAME, + CORNERSTONE_3D_TOOLS_SOURCE_VERSION + ); + if (!source) { + console.warn( + '[24x7-dental-ui] Cornerstone3DTools measurement source not found β€” dental mappings not registered.' + ); + return; + } + + const { displaySetService } = servicesManager.services; + const toAnnotation = () => {}; // no round-trip needed + + const lengthCriteria = [{ valueType: MeasurementService.VALUE_TYPES.POLYLINE, points: 2 }]; + const angleCriteria = [{ valueType: MeasurementService.VALUE_TYPES.ANGLE }]; + + measurementService.addMapping( + source, + 'PALength', + lengthCriteria, + toAnnotation, + makeLengthToMeasurement(displaySetService) + ); + measurementService.addMapping( + source, + 'CrownWidth', + lengthCriteria, + toAnnotation, + makeLengthToMeasurement(displaySetService) + ); + measurementService.addMapping( + source, + 'RootLength', + lengthCriteria, + toAnnotation, + makeLengthToMeasurement(displaySetService) + ); + measurementService.addMapping( + source, + 'CanalAngle', + angleCriteria, + toAnnotation, + makeAngleToMeasurement(displaySetService) + ); + + _registered = true; +} diff --git a/extensions/24x7-dental-ui/src/panels/PanelDentalMeasurements.tsx b/extensions/24x7-dental-ui/src/panels/PanelDentalMeasurements.tsx new file mode 100644 index 00000000000..08fea0c293c --- /dev/null +++ b/extensions/24x7-dental-ui/src/panels/PanelDentalMeasurements.tsx @@ -0,0 +1,241 @@ +import React, { useMemo, useState, useCallback } from 'react'; +import { useSystem } from '@ohif/core'; +import { + MeasurementTable, + ScrollArea, + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, + cn, +} from '@ohif/ui-next'; +import { + PanelMeasurement, + StudyMeasurements, + StudySummaryFromMetadata, + AccordionGroup, + StudyMeasurementsActions, + MeasurementsOrAdditionalFindings, +} from '@ohif/extension-cornerstone'; +import { AccordionTrigger } from '@ohif/ui-next'; + +export const DENTAL_TOOL_NAMES = new Set(['PALength', 'CanalAngle', 'CrownWidth', 'RootLength']); + +const FILTER_OPTIONS = [ + { value: 'all', label: 'All' }, + { value: 'PALength', label: 'PA Length' }, + { value: 'CanalAngle', label: 'Canal Angle' }, + { value: 'CrownWidth', label: 'Crown Width' }, + { value: 'RootLength', label: 'Root Length' }, +] as const; +type FilterValue = (typeof FILTER_OPTIONS)[number]['value']; + +const SORT_OPTIONS = [ + { value: 'newest', label: 'Newest First' }, + { value: 'oldest', label: 'Oldest First' }, + { value: 'az', label: 'Name A β†’ Z' }, + { value: 'za', label: 'Name Z β†’ A' }, +] as const; +type SortValue = (typeof SORT_OPTIONS)[number]['value']; +function SortWrapper({ + items = [], + sortOrder, + children, + ...rest +}: { + items?: any[]; + sortOrder: SortValue; + children: React.ReactElement; + [key: string]: any; +}) { + const sorted = useMemo(() => { + const arr = [...items]; + switch (sortOrder) { + case 'oldest': + return arr.reverse(); + case 'az': + return arr.sort((a, b) => + (a.label ?? a.toolName ?? '').localeCompare(b.label ?? b.toolName ?? '') + ); + case 'za': + return arr.sort((a, b) => + (b.label ?? b.toolName ?? '').localeCompare(a.label ?? a.toolName ?? '') + ); + case 'newest': + default: + return arr; + } + }, [items, sortOrder]); + + return React.cloneElement(React.Children.only(children), { ...rest, items: sorted }); +} + +function ControlsBar({ + activeFilter, + onFilterChange, + sortOrder, + onSortChange, + onDownload, +}: { + activeFilter: FilterValue; + onFilterChange: (v: FilterValue) => void; + sortOrder: SortValue; + onSortChange: (v: SortValue) => void; + onDownload: () => void; +}) { + return ( +
+
+ {FILTER_OPTIONS.map(opt => ( + + ))} +
+ +
+ + + +
+
+ ); +} + +function PanelDentalMeasurements(props: any) { + const { servicesManager } = useSystem(); + const { measurementService } = servicesManager.services; + + const [activeFilter, setActiveFilter] = useState('all'); + const [sortOrder, setSortOrder] = useState('newest'); + + const measurementFilter = useCallback( + (measurement: any) => { + if (!DENTAL_TOOL_NAMES.has(measurement.toolName)) return false; + if (activeFilter !== 'all' && measurement.toolName !== activeFilter) return false; + return true; + }, + [activeFilter] + ); + + const handleDownload = useCallback(() => { + if (!measurementService) return; + const raw = measurementService.getMeasurements(measurementFilter); + + const exportData = raw.map((m: any) => ({ + uid: m.uid, + label: m.label ?? null, + toolName: m.toolName, + value: m.displayText?.primary?.join(' ') ?? null, + location: m.displayText?.secondary?.join(' ') ?? null, + referenceSeriesUID: m.referenceSeriesUID ?? null, + referencedImageId: m.referencedImageId ?? null, + points: m.points ?? null, + data: m.data ?? null, + })); + + const blob = new Blob([JSON.stringify(exportData, null, 2)], { + type: 'application/json', + }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `dental-measurements-${Date.now()}.json`; + a.click(); + URL.revokeObjectURL(url); + }, [measurementService, measurementFilter]); + + const EmptyComponent = () => ( +
+ + + +
+ ); + + const actions = { + createSR: undefined, + onDelete: () => { + if (!measurementService) return; + measurementService + .getMeasurements() + .filter(m => DENTAL_TOOL_NAMES.has(m.toolName)) + .forEach(m => measurementService.remove(m.uid)); + }, + }; + + const Header = (headerProps: any) => ( + +
+ +
+
+ ); + + return ( +
+ + + +
+ + + + +
+ + + + + +
+
+
+ ); +} + +export default PanelDentalMeasurements; diff --git a/extensions/24x7-dental-ui/src/panels/PanelTrackedMeasurementsNoDental.tsx b/extensions/24x7-dental-ui/src/panels/PanelTrackedMeasurementsNoDental.tsx new file mode 100644 index 00000000000..bfbf0aae5a7 --- /dev/null +++ b/extensions/24x7-dental-ui/src/panels/PanelTrackedMeasurementsNoDental.tsx @@ -0,0 +1,136 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { useSystem, utils } from '@ohif/core'; +import { AccordionTrigger, MeasurementTable, ScrollArea, useViewportGrid } from '@ohif/ui-next'; +import { + PanelMeasurement, + StudyMeasurements, + StudySummaryFromMetadata, + AccordionGroup, + StudyMeasurementsActions, + MeasurementsOrAdditionalFindings, +} from '@ohif/extension-cornerstone'; +import { DENTAL_TOOL_NAMES } from './PanelDentalMeasurements'; + +const { filterMeasurementsBySeriesUID, filterAny } = utils.MeasurementFilters; + +function PanelTrackedMeasurementsNoDental(props: any) { + const [viewportGrid] = useViewportGrid(); + const { servicesManager } = useSystem(); + const { measurementService, uiModalService, trackedMeasurementsService } = + servicesManager.services as any; + + const [trackedSeries, setTrackedSeries] = useState( + () => trackedMeasurementsService?.getTrackedSeries() ?? [] + ); + + useEffect(() => { + if (!trackedMeasurementsService) return; + + const { unsubscribe } = trackedMeasurementsService.subscribe( + trackedMeasurementsService.EVENTS.TRACKED_SERIES_CHANGED, + ({ trackedSeries: series }: { trackedSeries: string[] }) => { + setTrackedSeries(series ?? []); + } + ); + + return unsubscribe; + }, [trackedMeasurementsService]); + + const baseFilter = trackedSeries.length + ? filterMeasurementsBySeriesUID(trackedSeries) + : filterAny; + + const measurementFilter = useCallback( + (measurement: any) => !DENTAL_TOOL_NAMES.has(measurement.toolName) && baseFilter?.(measurement), + [trackedSeries] + ); + + const onDelete = useCallback(() => { + const hasDirtyMeasurements = measurementService + .getMeasurements() + .filter(measurementFilter) + .some((m: any) => m.isDirty); + + const doDelete = () => { + measurementService + .getMeasurements() + .filter(measurementFilter) + .forEach((m: any) => measurementService.remove(m.uid)); + trackedMeasurementsService?.reset?.(); + }; + + if (hasDirtyMeasurements) { + uiModalService?.show({ + title: 'Untrack Study', + content: ({ onClose }: any) => ( +
+

Are you sure you want to untrack this study and delete all measurements?

+
+ + +
+
+ ), + }); + } else { + doDelete(); + } + }, [measurementService, measurementFilter, trackedMeasurementsService, uiModalService]); + + const EmptyComponent = () => ( +
+ + + +
+ ); + + const actions = { + createSR: undefined, + onDelete, + }; + + const Header = (headerProps: any) => ( + +
+ +
+
+ ); + + return ( + +
+ + + +
+ + + + +
+
+ ); +} + +export default PanelTrackedMeasurementsNoDental; diff --git a/extensions/24x7-dental-ui/src/styles/dental-theme.css b/extensions/24x7-dental-ui/src/styles/dental-theme.css new file mode 100644 index 00000000000..079765a257f --- /dev/null +++ b/extensions/24x7-dental-ui/src/styles/dental-theme.css @@ -0,0 +1,42 @@ +@import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,300;0,6..12,400;0,6..12,500;0,6..12,600;0,6..12,700;1,6..12,400&display=swap'); + +html.dental-theme { + --background: 210 22% 97%; + --foreground: 222 47% 11%; + --card: 0 0% 100%; + --card-foreground: 222 47% 11%; + --popover: 0 0% 100%; + --popover-foreground: 222 47% 11%; + --primary: 185 78% 35%; + --primary-foreground: 0 0% 100%; + --secondary: 210 40% 91%; + --secondary-foreground: 222 47% 11%; + --muted: 210 38% 95%; + --muted-foreground: 215 16% 43%; + --accent: 185 55% 88%; + --accent-foreground: 185 78% 20%; + --destructive: 0 65% 48%; + --destructive-foreground: 0 0% 100%; + --border: 214 30% 82%; + --input: 214 30% 89%; + --ring: 185 78% 35%; + --radius: 0.5rem; + --highlight: 185 80% 40%; + --neutral: 215 18% 48%; + --neutral-light: 215 55% 78%; + --neutral-dark: 215 28% 25%; +} +html.dental-theme, +html.dental-theme body, +html.dental-theme * { + font-family: 'Nunito Sans', 'Inter', 'Segoe UI', sans-serif; +} +html.dental-theme body { + letter-spacing: 0.01em; +} +html.dental-theme .ohif-scrollbar { + scrollbar-color: hsl(185 55% 65%) transparent; +} +html.dental-theme .ohif-scrollbar::-webkit-scrollbar-thumb { + background-color: hsl(185 55% 65%); +} diff --git a/extensions/24x7-dental-ui/src/tools/CanalAngleTool.ts b/extensions/24x7-dental-ui/src/tools/CanalAngleTool.ts new file mode 100644 index 00000000000..5ce640b6298 --- /dev/null +++ b/extensions/24x7-dental-ui/src/tools/CanalAngleTool.ts @@ -0,0 +1,7 @@ +import { AngleTool } from '@cornerstonejs/tools'; + +class CanalAngleTool extends AngleTool { + static toolName = 'CanalAngle'; +} + +export default CanalAngleTool; diff --git a/extensions/24x7-dental-ui/src/tools/CrownWidthTool.ts b/extensions/24x7-dental-ui/src/tools/CrownWidthTool.ts new file mode 100644 index 00000000000..7457f0d3144 --- /dev/null +++ b/extensions/24x7-dental-ui/src/tools/CrownWidthTool.ts @@ -0,0 +1,7 @@ +import { LengthTool } from '@cornerstonejs/tools'; + +class CrownWidthTool extends LengthTool { + static toolName = 'CrownWidth'; +} + +export default CrownWidthTool; diff --git a/extensions/24x7-dental-ui/src/tools/PALengthTool.ts b/extensions/24x7-dental-ui/src/tools/PALengthTool.ts new file mode 100644 index 00000000000..8a1dd75ceb9 --- /dev/null +++ b/extensions/24x7-dental-ui/src/tools/PALengthTool.ts @@ -0,0 +1,7 @@ +import { LengthTool } from '@cornerstonejs/tools'; + +class PALengthTool extends LengthTool { + static toolName = 'PALength'; +} + +export default PALengthTool; diff --git a/extensions/24x7-dental-ui/src/tools/RootLengthTool.ts b/extensions/24x7-dental-ui/src/tools/RootLengthTool.ts new file mode 100644 index 00000000000..5920f87992f --- /dev/null +++ b/extensions/24x7-dental-ui/src/tools/RootLengthTool.ts @@ -0,0 +1,7 @@ +import { LengthTool } from '@cornerstonejs/tools'; + +class RootLengthTool extends LengthTool { + static toolName = 'RootLength'; +} + +export default RootLengthTool; diff --git a/extensions/default/src/ViewerLayout/ViewerHeader.tsx b/extensions/default/src/ViewerLayout/ViewerHeader.tsx index eb3b7789d17..335d73e2c68 100644 --- a/extensions/default/src/ViewerLayout/ViewerHeader.tsx +++ b/extensions/default/src/ViewerLayout/ViewerHeader.tsx @@ -39,6 +39,10 @@ function ViewerHeader({ appConfig }: withAppTypes<{ appConfig: AppTypes.Config } const { t } = useTranslation(); const { show } = useModal(); + const UserInfoComponent = customizationService.getCustomization('ohif.userInfo') as + | React.ComponentType + | undefined; + const AboutModal = customizationService.getCustomization( 'ohif.aboutModal' ) as Types.MenuComponentCustomization; @@ -87,6 +91,7 @@ function ViewerHeader({ appConfig }: withAppTypes<{ appConfig: AppTypes.Config } isReturnEnabled={!!appConfig.showStudyList} onClickReturnButton={onClickReturnButton} WhiteLabeling={appConfig.whiteLabeling} + UserInfo={UserInfoComponent ? : undefined} Secondary={} PatientInfo={ appConfig.showPatientInfo !== PatientInfoVisibility.DISABLED && ( diff --git a/firebase.json b/firebase.json new file mode 100644 index 00000000000..e8312dc06bf --- /dev/null +++ b/firebase.json @@ -0,0 +1,22 @@ +{ + "hosting": { + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "rewrites": [ + { + "source": "**", + "run": { + "serviceId": "ohif-dental-viewer", + "region": "us-central1" + } + } + ], + "headers": [ + { + "source": "**/*.@(js|css|woff2|woff|ttf)", + "headers": [ + { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" } + ] + } + ] + } +} diff --git a/modes/24x7-dental-ui/.gitignore b/modes/24x7-dental-ui/.gitignore new file mode 100644 index 00000000000..67045665db2 --- /dev/null +++ b/modes/24x7-dental-ui/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/modes/24x7-dental-ui/.prettierrc b/modes/24x7-dental-ui/.prettierrc new file mode 100644 index 00000000000..ef83baaef93 --- /dev/null +++ b/modes/24x7-dental-ui/.prettierrc @@ -0,0 +1,11 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"], + "trailingComma": "es5", + "printWidth": 100, + "proseWrap": "always", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/modes/24x7-dental-ui/.webpack/webpack.prod.js b/modes/24x7-dental-ui/.webpack/webpack.prod.js new file mode 100644 index 00000000000..9f742f9c9f1 --- /dev/null +++ b/modes/24x7-dental-ui/.webpack/webpack.prod.js @@ -0,0 +1,100 @@ +const path = require('path'); +const pkg = require('../package.json'); + +const outputFile = 'index.umd.js'; +const rootDir = path.resolve(__dirname, '../'); +const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`); + +// Todo: add ESM build for the mode in addition to umd build +const config = { + mode: 'production', + entry: rootDir + '/' + pkg.module, + devtool: 'source-map', + output: { + path: outputFolder, + filename: outputFile, + library: pkg.name, + libraryTarget: 'umd', + chunkFilename: '[name].chunk.js', + umdNamedDefine: true, + globalObject: "typeof self !== 'undefined' ? self : this", + }, + externals: [ + { + react: { + root: 'React', + commonjs2: 'react', + commonjs: 'react', + amd: 'react', + }, + '@ohif/core': { + commonjs2: '@ohif/core', + commonjs: '@ohif/core', + amd: '@ohif/core', + root: '@ohif/core', + }, + '@ohif/ui': { + commonjs2: '@ohif/ui', + commonjs: '@ohif/ui', + amd: '@ohif/ui', + root: '@ohif/ui', + }, + '@ohif/mode-longitudinal': { + commonjs2: '@ohif/mode-longitudinal', + commonjs: '@ohif/mode-longitudinal', + amd: '@ohif/mode-longitudinal', + root: '@ohif/mode-longitudinal', + } + }, + ], + module: { + rules: [ + { + test: /\.svg?$/, + oneOf: [ + { + use: [ + { + loader: '@svgr/webpack', + options: { + svgoConfig: { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false + }, + }, + }, + ] + }, + prettier: false, + svgo: true, + titleProp: true, + }, + }, + ], + issuer: { + and: [/\.(ts|tsx|js|jsx|md|mdx)$/], + }, + }, + ], + }, + { + test: /(\.jsx|\.js|\.tsx|\.ts)$/, + loader: 'babel-loader', + exclude: /(node_modules|bower_components)/, + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, + ], + }, + resolve: { + modules: [path.resolve('./node_modules'), path.resolve('./src')], + extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'], + }, +}; + +module.exports = config; diff --git a/modes/24x7-dental-ui/LICENSE b/modes/24x7-dental-ui/LICENSE new file mode 100644 index 00000000000..102957d12de --- /dev/null +++ b/modes/24x7-dental-ui/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2026 24x7-dental-ui (dennis.jayvee.patricio.03@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/modes/24x7-dental-ui/README.md b/modes/24x7-dental-ui/README.md new file mode 100644 index 00000000000..996f4c60b19 --- /dev/null +++ b/modes/24x7-dental-ui/README.md @@ -0,0 +1,7 @@ +# 24x7-dental-ui +## Description +24x7 Mode for Dental SaaS UI customization for OHIF Viewe +## Author +Dennis Jayvee Patricio +## License +MIT \ No newline at end of file diff --git a/modes/24x7-dental-ui/babel.config.js b/modes/24x7-dental-ui/babel.config.js new file mode 100644 index 00000000000..24adaea8d29 --- /dev/null +++ b/modes/24x7-dental-ui/babel.config.js @@ -0,0 +1,43 @@ +module.exports = { + plugins: ['@babel/plugin-transform-class-properties'], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + '@babel/preset-typescript', + ], + '@babel/preset-react', + ], + plugins: [ + '@babel/plugin-transform-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/modes/24x7-dental-ui/package.json b/modes/24x7-dental-ui/package.json new file mode 100644 index 00000000000..ced1993e81a --- /dev/null +++ b/modes/24x7-dental-ui/package.json @@ -0,0 +1,61 @@ +{ + "name": "@ohif/mode-24x7-dental-ui", + "version": "1.0.0", + "description": "24x7 Mode for Dental SaaS UI customization for OHIF Viewe", + "author": "Dennis Jayvee Patricio", + "license": "MIT", + "main": "src/index.tsx", + "files": [ + "dist/**", + "public/**", + "README.md" + ], + "repository": "OHIF/Viewers", + "keywords": [ + "ohif-mode" + ], + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "scripts": { + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:cornerstone": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "^3.13.0-beta.56" + }, + "dependencies": { + "@babel/runtime": "^7.20.13" + }, + "devDependencies": { + "@babel/core": "7.28.0", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-regenerator": "^7.16.7", + "@babel/plugin-transform-runtime": "7.28.0", + "@babel/plugin-transform-typescript": "^7.28.0", + "@babel/preset-env": "7.28.0", + "@babel/preset-react": "^7.27.1", + "@babel/preset-typescript": "^7.27.1", + "babel-loader": "^8.0.0-beta.4", + "@svgr/webpack": "^8.1.0", + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^10.2.0", + "cross-env": "^7.0.3", + "dotenv": "^14.1.0", + "webpack": "5.89.0", + "webpack-merge": "^5.7.3", + "webpack-cli": "^5.0.2" + } +} diff --git a/modes/24x7-dental-ui/src/i18n/locales/en-US/Modes.json b/modes/24x7-dental-ui/src/i18n/locales/en-US/Modes.json new file mode 100644 index 00000000000..a19dab6f4ba --- /dev/null +++ b/modes/24x7-dental-ui/src/i18n/locales/en-US/Modes.json @@ -0,0 +1,3 @@ +{ + "Dental View": "Dental Viewer" +} diff --git a/modes/24x7-dental-ui/src/id.js b/modes/24x7-dental-ui/src/id.js new file mode 100644 index 00000000000..ebe5acd98ae --- /dev/null +++ b/modes/24x7-dental-ui/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/modes/24x7-dental-ui/src/index.tsx b/modes/24x7-dental-ui/src/index.tsx new file mode 100644 index 00000000000..b555543e39a --- /dev/null +++ b/modes/24x7-dental-ui/src/index.tsx @@ -0,0 +1,284 @@ +import i18n from 'i18next'; +import { ToolbarService } from '@ohif/core'; +import { id } from './id'; +import { + initToolGroups, + toolbarButtons as basicToolbarButtons, + toolbarSections as basicToolbarSections, + onModeExit as basicOnModeExit, + onModeEnter as basicOnModeEnter, + cornerstone, + basicLayout, + basicRoute, + extensionDependencies as basicDependencies, + mode as basicMode, + modeInstance as basicModeInstance, +} from '@ohif/mode-basic'; +import dentalTranslations from './i18n/locales/en-US/Modes.json'; +import { registerDentalMappings } from '@ohif/extension-24x7-dental-ui/src/measurements/registerDentalMappings'; + +const { TOOLBAR_SECTIONS } = ToolbarService; + +const namespaces = ['en-US', 'en-GB', 'en']; +namespaces.forEach(locale => { + i18n.addResourceBundle(locale, 'Modes', dentalTranslations, true, true); +}); + +export const tracked = { + measurements: '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', + thumbnailList: '@ohif/extension-measurement-tracking.panelModule.seriesList', + viewport: '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked', +}; + +export const dental = { + measurements: '@ohif/extension-24x7-dental-ui.panelModule.dentalMeasurements', + trackedMeasurements: '@ohif/extension-24x7-dental-ui.panelModule.trackedMeasurementsNoDental', + viewerLayout: '@ohif/extension-24x7-dental-ui.layoutTemplateModule.dentalViewerLayout', +}; + +export const extensionDependencies = { + ...basicDependencies, + '@ohif/extension-measurement-tracking': '^3.0.0', + '@ohif/extension-24x7-dental-ui': '^1.0.0', +}; + +export const dentalInstance = { + ...basicLayout, + id: dental.viewerLayout, + props: { + ...basicLayout.props, + leftPanels: [tracked.thumbnailList], + rightPanels: [cornerstone.segmentation, dental.trackedMeasurements, dental.measurements], + viewports: [ + { + namespace: tracked.viewport, + displaySetsToDisplay: basicLayout.props.viewports[0].displaySetsToDisplay, + }, + ...basicLayout.props.viewports, + ], + }, +}; + +export const dentalRoute = { + ...basicRoute, + path: 'dental', + layoutInstance: dentalInstance, +}; + +const setToolActiveToolbar = { + commandName: 'setToolActiveToolbar', + commandOptions: { + toolGroupIds: ['default', 'mpr', 'SRToolGroup', 'volume3d'], + }, +}; + +const dentalThemeToggleButton = { + id: 'DentalThemeToggle', + uiType: 'dental.themeToggle', + props: {}, +} as const; + +const dentalBrandTitleButton = { + id: 'DentalBrandTitle', + uiType: 'dental.brandTitle', + props: {}, +} as const; + +const toothSelectorButton = { + id: 'ToothSelector', + uiType: 'dental.toothSelector', + props: {}, +} as const; + +const dentalToolsSectionButton = { + id: 'DentalTools', + uiType: 'ohif.toolButtonList', + props: { + buttonSection: true, + }, +}; + +const paLengthButton = { + id: 'PALength', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-length', + label: 'PA Length', + tooltip: 'PA Length Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, +}; + +const canalAngleButton = { + id: 'CanalAngle', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-angle', + label: 'Canal Angle', + tooltip: 'Canal Angle Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, +}; + +const crownWidthButton = { + id: 'CrownWidth', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-length', + label: 'Crown Width (mm)', + tooltip: 'Crown Width Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, +}; + +const rootLengthButton = { + id: 'RootLength', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-length', + label: 'Root Length (mm)', + tooltip: 'Root Length Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, +}; + +export const toolbarButtons = [ + ...basicToolbarButtons, + dentalThemeToggleButton, + dentalBrandTitleButton, + toothSelectorButton, + dentalToolsSectionButton, + paLengthButton, + canalAngleButton, + crownWidthButton, + rootLengthButton, +]; + +export const toolbarSections = { + ...basicToolbarSections, + [TOOLBAR_SECTIONS.secondary]: ['DentalBrandTitle'], + [TOOLBAR_SECTIONS.primary]: [ + ...basicToolbarSections[TOOLBAR_SECTIONS.primary], + 'DentalTools', + 'ToothSelector', + 'DentalThemeToggle', + ], + DentalTools: ['PALength', 'CanalAngle', 'CrownWidth', 'RootLength'], +}; + +const DENTAL_TOOL_BASE_LABELS: Record = { + PALength: 'PA Length', + CanalAngle: 'Canal Angle', + CrownWidth: 'Crown Width', + RootLength: 'Root Length', +}; + +function onModeEnter(this: typeof modeInstance, args: withAppTypes): void { + // Run all basic-mode initialisation (tool groups, toolbar registration, etc.) + basicOnModeEnter.call(this, args); + + const servicesManager = args?.servicesManager; + if (!servicesManager) return; + + const measurementService = servicesManager.services?.measurementService as any; + const toolGroupService = servicesManager.services?.toolGroupService as any; + const panelService = servicesManager.services?.panelService as any; + + registerDentalMappings(measurementService, servicesManager); + + const dentalTools: any = { + passive: [ + { toolName: 'PALength' }, + { toolName: 'CanalAngle' }, + { toolName: 'CrownWidth' }, + { toolName: 'RootLength' }, + ], + }; + + if (toolGroupService) { + for (const groupId of ['default', 'mpr']) { + try { + toolGroupService.addToolsToToolGroup(groupId, dentalTools); + } catch { + // silently skip. + } + } + } + + if (!measurementService) return; + + const { unsubscribe } = measurementService.subscribe( + measurementService.EVENTS.MEASUREMENT_ADDED, + (data: any) => { + const { measurement } = data; + const { toolName, uid } = measurement; + const baseLabel = DENTAL_TOOL_BASE_LABELS[toolName]; + if (!baseLabel) return; + + const count: number = measurementService + .getMeasurements() + .filter((m: any) => m.toolName === toolName).length; + + measurementService.update(uid, { ...measurement, label: `${baseLabel} ${count}` }, true); + } + ); + + const panelTriggerSubs = + panelService?.addActivatePanelTriggers( + dental.measurements, + [ + { + sourcePubSubService: measurementService, + sourceEvents: [measurementService.EVENTS.MEASUREMENT_ADDED], + }, + ], + false + ) ?? []; + (this as any)._dentalSubscriptions = [ + unsubscribe, + ...panelTriggerSubs.map((s: any) => s.unsubscribe ?? s), + ]; +} + +function onModeExit(this: typeof modeInstance, args: withAppTypes): void { + const subs = (this as any)._dentalSubscriptions as Array<() => void>; + if (subs?.length) { + subs.forEach(unsub => typeof unsub === 'function' && unsub()); + (this as any)._dentalSubscriptions = []; + } + basicOnModeExit.call(this, args); + document.documentElement.classList.remove('dental-theme'); +} + +export const modeInstance = { + ...basicModeInstance, + id, + routeName: 'dental', + displayName: i18n.t('Modes:Dental View'), + hangingProtocol: '@24x7-dental-ui/hp2x2', + config: { + ...basicModeInstance.config, + showPatientInfo: 'visible', + }, + routes: [dentalRoute], + extensions: extensionDependencies, + toolbarButtons, + toolbarSections, + onModeEnter, + onModeExit, + _dentalSubscriptions: [] as Array<() => void>, +}; + +const mode = { + ...basicMode, + id, + modeInstance, + extensionDependencies, +}; + +export default mode; +export { initToolGroups }; diff --git a/platform/app/.env b/platform/app/.env index 5c17741b265..a201e8960da 100644 --- a/platform/app/.env +++ b/platform/app/.env @@ -9,3 +9,10 @@ PUBLIC_URL=/ APP_CONFIG=config/default.js USE_HASH_ROUTER=false + +REACT_APP_FIREBASE_API_KEY= +REACT_APP_FIREBASE_AUTH_DOMAIN= +REACT_APP_FIREBASE_PROJECT_ID= +REACT_APP_FIREBASE_STORAGE_BUCKET= +REACT_APP_FIREBASE_MESSAGING_SENDER_ID= +REACT_APP_FIREBASE_APP_ID= diff --git a/platform/app/.gitignore b/platform/app/.gitignore index 9062b1c2868..e8424226b20 100644 --- a/platform/app/.gitignore +++ b/platform/app/.gitignore @@ -1,3 +1,8 @@ .vercel test-results store.json + +# Environment variables +.env +.env.local +.env.*.local diff --git a/platform/app/.webpack/webpack.pwa.js b/platform/app/.webpack/webpack.pwa.js index 2bbfbc916f3..0a32168212b 100644 --- a/platform/app/.webpack/webpack.pwa.js +++ b/platform/app/.webpack/webpack.pwa.js @@ -80,6 +80,14 @@ module.exports = (env, argv) => { // Hoisted Yarn Workspace Modules path.resolve(__dirname, '../../../node_modules'), SRC_DIR, + path.resolve( + __dirname, + 'C:/Users/denni/Documents/Projects/24x7-ohif/modes/24x7-dental-ui/node_modules' + ), + path.resolve( + __dirname, + 'C:/Users/denni/Documents/Projects/24x7-ohif/extensions/24x7-dental-ui/node_modules' + ), ], }, plugins: [ diff --git a/platform/app/package.json b/platform/app/package.json index 62c58a76657..dbda5d3b802 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -104,6 +104,7 @@ "react-shepherd": "6.1.1", "shepherd.js": "13.0.3", "url-loader": "4.1.1", + "firebase": "11.6.0", "zustand": "4.5.5" }, "devDependencies": { diff --git a/platform/app/pluginConfig.json b/platform/app/pluginConfig.json index fd56a748cf5..b365257c733 100644 --- a/platform/app/pluginConfig.json +++ b/platform/app/pluginConfig.json @@ -65,6 +65,10 @@ { "packageName": "@ohif/extension-ultrasound-pleura-bline", "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-24x7-dental-ui", + "version": "1.0.0" } ], "modes": [ @@ -99,6 +103,10 @@ { "packageName": "@ohif/mode-ultrasound-pleura-bline", "version": "3.0.0" + }, + { + "packageName": "@ohif/mode-24x7-dental-ui", + "version": "1.0.0" } ], "public": [ diff --git a/platform/app/src/App.tsx b/platform/app/src/App.tsx index 446308d0092..f0c81557690 100644 --- a/platform/app/src/App.tsx +++ b/platform/app/src/App.tsx @@ -34,6 +34,8 @@ import { AppConfigProvider } from '@state'; import createRoutes from './routes'; import appInit from './appInit.js'; import OpenIdConnectRoutes from './utils/OpenIdConnectRoutes'; +import FirebaseAuthRoutes from './utils/FirebaseAuthRoutes'; +import FirebaseUserInfo from './components/FirebaseUserInfo'; import { ShepherdJourneyProvider } from 'react-shepherd'; import './App.css'; @@ -157,7 +159,7 @@ function App({ showStudyList, }); - if (oidc) { + if (oidc?.length) { authRoutes = ( {authRoutes} - {appRoutes} + {firebaseEnabled ? ( + + {appRoutes} + + ) : ( + appRoutes + )} ); diff --git a/platform/app/src/components/FirebaseUserInfo.tsx b/platform/app/src/components/FirebaseUserInfo.tsx new file mode 100644 index 00000000000..573a4cf8f0a --- /dev/null +++ b/platform/app/src/components/FirebaseUserInfo.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { + useUserAuthentication, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuItem, +} from '@ohif/ui-next'; +import { getAuth, signOut } from 'firebase/auth'; + +function LogoutIcon(): React.ReactElement { + return ( + + + + + + ); +} + +export default function FirebaseUserInfo(): React.ReactElement { + const authContext = useUserAuthentication() as unknown as [{ user: any; enabled: boolean }, any]; + const user = authContext?.[0]?.user; + + if (!user) { + return <>; + } + + const displayName: string = user.displayName || user.email || 'User'; + const email: string = user.email || ''; + const photoURL: string | null = user.photoURL ?? null; + const initials: string = displayName.charAt(0).toUpperCase(); + + const handleLogout = async () => { + try { + await signOut(getAuth()); + } catch (err) { + console.error('Sign-out error:', err); + } + }; + + return ( + + + + + + + + {displayName} + {email && ( + {email} + )} + + + + + + + Sign out + + + + ); +} diff --git a/platform/app/src/routes/LoginPage.tsx b/platform/app/src/routes/LoginPage.tsx new file mode 100644 index 00000000000..e0f15e4f570 --- /dev/null +++ b/platform/app/src/routes/LoginPage.tsx @@ -0,0 +1,80 @@ +import React, { useState } from 'react'; + +interface LoginPageProps { + onSignIn: () => void; +} + +function LoginPage({ onSignIn }: LoginPageProps) { + const [isSigningIn, setIsSigningIn] = useState(false); + const [error, setError] = useState(null); + + const handleGoogleSignIn = async () => { + setIsSigningIn(true); + setError(null); + try { + onSignIn(); + } catch (err: any) { + console.error('Google sign-in error:', err); + setError(err.message || 'Sign-in failed. Please try again.'); + setIsSigningIn(false); + } + }; + + return ( +
+
+
+ OHIF Logo { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> +

24x7 Dental Viewer

+

Sign in to continue

+
+ + {error && ( +
+ {error} +
+ )} + + +
+
+ ); +} + +export default LoginPage; diff --git a/platform/app/src/routes/WorkList/WorkList.tsx b/platform/app/src/routes/WorkList/WorkList.tsx index 9c97426ea6f..6ed68d6f55f 100644 --- a/platform/app/src/routes/WorkList/WorkList.tsx +++ b/platform/app/src/routes/WorkList/WorkList.tsx @@ -39,6 +39,7 @@ import { import { Types } from '@ohif/ui'; import { preserveQueryParameters, preserveQueryStrings } from '../../utils/preserveQueryParameters'; +import FirebaseUserInfo from '../../components/FirebaseUserInfo'; const PatientInfoVisibility = Types.PatientInfoVisibility; @@ -62,9 +63,7 @@ function WorkList({ }: withAppTypes) { const { show, hide } = useModal(); const { t } = useTranslation(); - // ~ Modes const [appConfig] = useAppConfig(); - // ~ Filters const searchParams = useSearchParams(); const navigate = useNavigate(); const STUDIES_LIMIT = 101; @@ -559,6 +558,7 @@ function WorkList({ isReturnEnabled={false} WhiteLabeling={appConfig.whiteLabeling} showPatientInfo={PatientInfoVisibility.DISABLED} + UserInfo={} /> diff --git a/platform/app/src/utils/FirebaseAuthRoutes.tsx b/platform/app/src/utils/FirebaseAuthRoutes.tsx new file mode 100644 index 00000000000..ce5db64ee02 --- /dev/null +++ b/platform/app/src/utils/FirebaseAuthRoutes.tsx @@ -0,0 +1,79 @@ +import React, { useEffect, useState } from 'react'; +import { Route, Routes } from 'react-router'; +import { + GoogleAuthProvider, + onAuthStateChanged, + signInWithPopup, + signOut, + User, +} from 'firebase/auth'; +import { firebaseAuth } from './firebaseConfig'; +import LoginPage from '../routes/LoginPage'; + +interface FirebaseAuthRoutesProps { + userAuthenticationService: AppTypes.UserAuthenticationService; + children: React.ReactNode; +} + +function FirebaseAuthRoutes({ userAuthenticationService, children }: FirebaseAuthRoutesProps) { + const [firebaseUser, setFirebaseUser] = useState(null); + const getAuthorizationHeader = async () => { + const currentUser = firebaseAuth.currentUser; + if (!currentUser) return {}; + try { + const token = await currentUser.getIdToken(false); + return { Authorization: `Bearer ${token}` }; + } catch { + return {}; + } + }; + + const handleSignIn = async () => { + try { + const provider = new GoogleAuthProvider(); + await signInWithPopup(firebaseAuth, provider); + } catch (err: any) { + console.error('Firebase sign-in error:', err.code, err.message); + } + }; + + useEffect(() => { + userAuthenticationService.set({ enabled: true }); + userAuthenticationService.setServiceImplementation({ + getAuthorizationHeader, + handleUnauthenticated: () => null, + reset: () => signOut(firebaseAuth), + }); + }, []); + + useEffect(() => { + const unsubscribe = onAuthStateChanged(firebaseAuth, user => { + userAuthenticationService.setUser(user); + setFirebaseUser(user ?? false); + }); + return () => unsubscribe(); + }, []); + + if (firebaseUser === null) { + return ( +
+

Loading…

+
+ ); + } + + if (firebaseUser === false) { + return ( + + } + /> + + ); + } + + return <>{children}; +} + +export default FirebaseAuthRoutes; diff --git a/platform/app/src/utils/firebaseConfig.ts b/platform/app/src/utils/firebaseConfig.ts new file mode 100644 index 00000000000..d4aef2fa72c --- /dev/null +++ b/platform/app/src/utils/firebaseConfig.ts @@ -0,0 +1,16 @@ +import { initializeApp, getApps, getApp } from 'firebase/app'; +import { getAuth } from 'firebase/auth'; + +const firebaseConfig = { + apiKey: process.env.REACT_APP_FIREBASE_API_KEY, + authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, + projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, + storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.REACT_APP_FIREBASE_APP_ID, +}; + +const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApp(); + +export const firebaseAuth = getAuth(app); +export default app; diff --git a/platform/ui-next/src/components/Header/Header.tsx b/platform/ui-next/src/components/Header/Header.tsx index 35964b6c6a7..375bf6c04a9 100644 --- a/platform/ui-next/src/components/Header/Header.tsx +++ b/platform/ui-next/src/components/Header/Header.tsx @@ -31,6 +31,7 @@ interface HeaderProps { PatientInfo?: ReactNode; Secondary?: ReactNode; UndoRedo?: ReactNode; + UserInfo?: ReactNode; } function Header({ @@ -43,6 +44,7 @@ function Header({ PatientInfo, UndoRedo, Secondary, + UserInfo, ...props }: HeaderProps): ReactNode { const onClickReturn = () => { @@ -119,6 +121,8 @@ function Header({
+
+ {UserInfo} diff --git a/yarn.lock b/yarn.lock index 0d63ef66e72..a8b23a68bdc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -591,7 +591,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.28.6" -"@babel/plugin-syntax-dynamic-import@7.8.3": +"@babel/plugin-syntax-dynamic-import@7.8.3", "@babel/plugin-syntax-dynamic-import@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== @@ -732,7 +732,7 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-arrow-functions@7.27.1", "@babel/plugin-transform-arrow-functions@^7.27.1": +"@babel/plugin-transform-arrow-functions@7.27.1", "@babel/plugin-transform-arrow-functions@^7.16.7", "@babel/plugin-transform-arrow-functions@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a" integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA== @@ -1351,6 +1351,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" +"@babel/plugin-transform-regenerator@^7.16.7", "@babel/plugin-transform-regenerator@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz#dec237cec1b93330876d6da9992c4abd42c9d18b" + integrity sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/plugin-transform-regenerator@^7.27.1": version "7.27.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.5.tgz#0c01f4e0e4cced15f68ee14b9c76dac9813850c7" @@ -1365,13 +1372,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-regenerator@^7.29.0": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz#dec237cec1b93330876d6da9992c4abd42c9d18b" - integrity sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog== - dependencies: - "@babel/helper-plugin-utils" "^7.28.6" - "@babel/plugin-transform-regexp-modifiers@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz#df9ba5577c974e3f1449888b70b76169998a6d09" @@ -1485,7 +1485,7 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" "@babel/plugin-syntax-typescript" "^7.27.1" -"@babel/plugin-transform-typescript@^7.28.5": +"@babel/plugin-transform-typescript@^7.28.0", "@babel/plugin-transform-typescript@^7.28.5": version "7.28.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz#1e93d96da8adbefdfdade1d4956f73afa201a158" integrity sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw== @@ -1791,7 +1791,7 @@ "@babel/plugin-transform-react-jsx-development" "^7.27.1" "@babel/plugin-transform-react-pure-annotations" "^7.27.1" -"@babel/preset-react@^7.16.0": +"@babel/preset-react@^7.16.0", "@babel/preset-react@^7.27.1": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.28.5.tgz#6fcc0400fa79698433d653092c3919bb4b0878d9" integrity sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ== @@ -1814,7 +1814,7 @@ "@babel/plugin-transform-modules-commonjs" "^7.27.1" "@babel/plugin-transform-typescript" "^7.27.1" -"@babel/preset-typescript@^7.16.0": +"@babel/preset-typescript@^7.16.0", "@babel/preset-typescript@^7.27.1": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz#540359efa3028236958466342967522fd8f2a60c" integrity sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g== @@ -1855,6 +1855,11 @@ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b" integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA== +"@babel/runtime@^7.20.13": + version "7.29.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.2.tgz#9a6e2d05f4b6692e1801cd4fb176ad823930ed5e" + integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g== + "@babel/runtime@^7.28.2": version "7.28.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" @@ -2414,6 +2419,397 @@ "@eslint/core" "^0.17.0" levn "^0.4.1" +"@firebase/analytics-compat@0.2.18": + version "0.2.18" + resolved "https://registry.yarnpkg.com/@firebase/analytics-compat/-/analytics-compat-0.2.18.tgz#5ea9bdea188d4c91938f539af4c4414813ddc5c9" + integrity sha512-Hw9mzsSMZaQu6wrTbi3kYYwGw9nBqOHr47pVLxfr5v8CalsdrG5gfs9XUlPOZjHRVISp3oQrh1j7d3E+ulHPjQ== + dependencies: + "@firebase/analytics" "0.10.12" + "@firebase/analytics-types" "0.8.3" + "@firebase/component" "0.6.13" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/analytics-types@0.8.3": + version "0.8.3" + resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.8.3.tgz#d08cd39a6209693ca2039ba7a81570dfa6c1518f" + integrity sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg== + +"@firebase/analytics@0.10.12": + version "0.10.12" + resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.10.12.tgz#b49b9351b8cb19da007320a52932b953fff90d71" + integrity sha512-iDCGnw6qdFqwI5ywkgece99WADJNoymu+nLIQI4fZM/vCZ3bEo4wlpEetW71s1HqGpI0hQStiPhqVjFxDb2yyw== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/installations" "0.6.13" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/app-check-compat@0.3.20": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@firebase/app-check-compat/-/app-check-compat-0.3.20.tgz#0dfce42699402f3b621be98857a8109b9397a37b" + integrity sha512-/twgmlnNAaZ/wbz3kcQrL/26b+X+zUX+lBmu5LwwEcWcpnb+mrVEAKhD7/ttm52dxYiSWtLDeuXy3FXBhqBC5A== + dependencies: + "@firebase/app-check" "0.8.13" + "@firebase/app-check-types" "0.5.3" + "@firebase/component" "0.6.13" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/app-check-interop-types@0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz#ed9c4a4f48d1395ef378f007476db3940aa5351a" + integrity sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A== + +"@firebase/app-check-types@0.5.3": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@firebase/app-check-types/-/app-check-types-0.5.3.tgz#38ba954acf4bffe451581a32fffa20337f11d8e5" + integrity sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng== + +"@firebase/app-check@0.8.13": + version "0.8.13" + resolved "https://registry.yarnpkg.com/@firebase/app-check/-/app-check-0.8.13.tgz#20b212d0ea5b79c9492f434abc276d4f28b19371" + integrity sha512-ONsgml8/dplUOAP42JQO6hhiWDEwR9+RUTLenxAN9S8N6gel/sDQ9Ci721Py1oASMGdDU8v9R7xAZxzvOX5lPg== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/app-compat@0.2.53": + version "0.2.53" + resolved "https://registry.yarnpkg.com/@firebase/app-compat/-/app-compat-0.2.53.tgz#ad5c520a2ea6df4cff0a8b4d5cc14c03c0d7bc57" + integrity sha512-vDeZSit0q4NyaDIVcaiJF3zhLgguP6yc0JwQAfpTyllgt8XMtkMFyY/MxJtFrK2ocpQX/yCbV2DXwvpY2NVuJw== + dependencies: + "@firebase/app" "0.11.4" + "@firebase/component" "0.6.13" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/app-types@0.9.3": + version "0.9.3" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.3.tgz#8408219eae9b1fb74f86c24e7150a148460414ad" + integrity sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw== + +"@firebase/app@0.11.4": + version "0.11.4" + resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.11.4.tgz#93f2637ed5b8dbc1ddf879c727d66a00c656c959" + integrity sha512-GPREsZjfSaHzwyC6cI/Cqvzf6zxqMzya+25tSpUstdqC2w0IdfxEfOMjfdW7bDfVEf4Rb4Nb6gfoOAgVSp4c4g== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.0" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/auth-compat@0.5.20": + version "0.5.20" + resolved "https://registry.yarnpkg.com/@firebase/auth-compat/-/auth-compat-0.5.20.tgz#b17755e874f1f0fe5e2b638c462bbdfb519526f4" + integrity sha512-8FwODTSBnaqGQbKfML7LcpzGGPyouB7YHg3dZq+CZMziVc7oBY1jJeNvpnM1hAQoVuTjWPXoRrCltdGeOlkKfQ== + dependencies: + "@firebase/auth" "1.10.0" + "@firebase/auth-types" "0.13.0" + "@firebase/component" "0.6.13" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/auth-interop-types@0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz#176a08686b0685596ff03d7879b7e4115af53de0" + integrity sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA== + +"@firebase/auth-types@0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.13.0.tgz#ae6e0015e3bd4bfe18edd0942b48a0a118a098d9" + integrity sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg== + +"@firebase/auth@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-1.10.0.tgz#eabfe747700a835a89c0069177ac58c40bfa6153" + integrity sha512-S7SqBsN7sIQsftNE3bitLlK+4bWrTHY+Rx2JFlNitgVYu2nK8W8ZQrkG8GCEwiFPq0B2vZ9pO5kVTFfq2sP96A== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/component@0.6.13": + version "0.6.13" + resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.6.13.tgz#6513379f1c09264133969d87282ce0d5bbbb2cd9" + integrity sha512-I/Eg1NpAtZ8AAfq8mpdfXnuUpcLxIDdCDtTzWSh+FXnp/9eCKJ3SNbOCKrUCyhLzNa2SiPJYruei0sxVjaOTeg== + dependencies: + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/data-connect@0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@firebase/data-connect/-/data-connect-0.3.3.tgz#644e67c248ceccbed749b1eb10418079783c5542" + integrity sha512-JsgppNX1wcQYP5bg4Sg6WTS7S0XazklSjr1fG3ox9DHtt4LOQwJ3X1/c81mKMIZxocV22ujiwLYQWG6Y9D1FiQ== + dependencies: + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.6.13" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/database-compat@2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-2.0.5.tgz#110f612901995f9800f2435f58686e0c6f3d2544" + integrity sha512-CNf1UbvWh6qIaSf4sn6sx2DTDz/em/D7QxULH1LTxxDQHr9+CeYGvlAqrKnk4ZH0P0eIHyQFQU7RwkUJI0B9gQ== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/database" "1.0.14" + "@firebase/database-types" "1.0.10" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/database-types@1.0.10": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-1.0.10.tgz#14cfed45bb06394cf1641e19265cbf90e4f6fb51" + integrity sha512-mH6RC1E9/Pv8jf1/p+M8YFTX+iu+iHDN89hecvyO7wHrI4R1V0TXjxOHvX3nLJN1sfh0CWG6CHZ0VlrSmK/cwg== + dependencies: + "@firebase/app-types" "0.9.3" + "@firebase/util" "1.11.0" + +"@firebase/database@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-1.0.14.tgz#1d579b345c0f926eaddb7703051999489300c3bd" + integrity sha512-9nxYtkHAG02/Nh2Ssms1T4BbWPPjiwohCvkHDUl4hNxnki1kPgsLo5xe9kXNzbacOStmVys+RUXvwzynQSKmUQ== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.6.13" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.0" + faye-websocket "0.11.4" + tslib "^2.1.0" + +"@firebase/firestore-compat@0.3.45": + version "0.3.45" + resolved "https://registry.yarnpkg.com/@firebase/firestore-compat/-/firestore-compat-0.3.45.tgz#93061f7d3644cd511749c9268d146e8c8c49a5de" + integrity sha512-uRvi7AYPmsDl7UZwPyV7jgDGYusEZ2+U2g7MndbQHKIA8fNHpYC6QrzMs58+/IjX+kF/lkUn67Vrr0AkVjlY+Q== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/firestore" "4.7.10" + "@firebase/firestore-types" "3.0.3" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/firestore-types@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-3.0.3.tgz#7d0c3dd8850c0193d8f5ee0cc8f11961407742c1" + integrity sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q== + +"@firebase/firestore@4.7.10": + version "4.7.10" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-4.7.10.tgz#6fe0cd31fcd7f4a8e13f9585f53e9300cf3114c0" + integrity sha512-6nKsyo2U+jYSCcSE5sjMdDNA23DMUvYPUvsYGg09CNvcTO8GGKsPs7SpOhspsB91mbacq+u627CDAx3FUhPSSQ== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.0" + "@firebase/webchannel-wrapper" "1.0.3" + "@grpc/grpc-js" "~1.9.0" + "@grpc/proto-loader" "^0.7.8" + tslib "^2.1.0" + +"@firebase/functions-compat@0.3.20": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@firebase/functions-compat/-/functions-compat-0.3.20.tgz#addf89242be8b4d63feacb15ef27785e16c5e220" + integrity sha512-iIudmYDAML6n3c7uXO2YTlzra2/J6lnMzmJTXNthvrKVMgNMaseNoQP1wKfchK84hMuSF8EkM4AvufwbJ+Juew== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/functions" "0.12.3" + "@firebase/functions-types" "0.6.3" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/functions-types@0.6.3": + version "0.6.3" + resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.6.3.tgz#f5faf770248b13f45d256f614230da6a11bfb654" + integrity sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg== + +"@firebase/functions@0.12.3": + version "0.12.3" + resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.12.3.tgz#b3e395aed1641d0d7169a22698c684a2745e02dd" + integrity sha512-Wv7JZMUkKLb1goOWRtsu3t7m97uK6XQvjQLPvn8rncY91+VgdU72crqnaYCDI/ophNuBEmuK8mn0/pAnjUeA6A== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.6.13" + "@firebase/messaging-interop-types" "0.2.3" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/installations-compat@0.2.13": + version "0.2.13" + resolved "https://registry.yarnpkg.com/@firebase/installations-compat/-/installations-compat-0.2.13.tgz#0db0867ed58b782f7e2d142d9289a9eef1da24d5" + integrity sha512-f/o6MqCI7LD/ulY9gvgkv6w5k6diaReD8BFHd/y/fEdpsXmFWYS/g28GXCB72bRVBOgPpkOUNl+VsMvDwlRKmw== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/installations" "0.6.13" + "@firebase/installations-types" "0.5.3" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/installations-types@0.5.3": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.5.3.tgz#cac8a14dd49f09174da9df8ae453f9b359c3ef2f" + integrity sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA== + +"@firebase/installations@0.6.13": + version "0.6.13" + resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.6.13.tgz#a2a00aebb5dfb74fae08600ea98cd2681211dd3c" + integrity sha512-6ZpkUiaygPFwgVneYxuuOuHnSPnTA4KefLEaw/sKk/rNYgC7X6twaGfYb0sYLpbi9xV4i5jXsqZ3WO+yaguNgg== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/util" "1.11.0" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/logger@0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.4.4.tgz#29e8379d20fd1149349a195ee6deee4573a86f48" + integrity sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g== + dependencies: + tslib "^2.1.0" + +"@firebase/messaging-compat@0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@firebase/messaging-compat/-/messaging-compat-0.2.17.tgz#fc223495319fe3784347dea77094b6e03548647d" + integrity sha512-5Q+9IG7FuedusdWHVQRjpA3OVD9KUWp/IPegcv0s5qSqRLBjib7FlAeWxN+VL0Ew43tuPJBY2HKhEecuizmO1Q== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/messaging" "0.12.17" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/messaging-interop-types@0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz#e647c9cd1beecfe6a6e82018a6eec37555e4da3e" + integrity sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q== + +"@firebase/messaging@0.12.17": + version "0.12.17" + resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.12.17.tgz#41eaeee70a89136715a4f7cb2b1a602423fc44ec" + integrity sha512-W3CnGhTm6Nx8XGb6E5/+jZTuxX/EK8Vur4QXvO1DwZta/t0xqWMRgO9vNsZFMYBqFV4o3j4F9qK/iddGYwWS6g== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/installations" "0.6.13" + "@firebase/messaging-interop-types" "0.2.3" + "@firebase/util" "1.11.0" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/performance-compat@0.2.15": + version "0.2.15" + resolved "https://registry.yarnpkg.com/@firebase/performance-compat/-/performance-compat-0.2.15.tgz#4e5034add63917cb6357938126e9e6562d0e7208" + integrity sha512-wUxsw7hGBEMN6XfvYQqwPIQp5LcJXawWM5tmYp6L7ClCoTQuEiCKHWWVurJgN8Q1YHzoHVgjNfPQAOVu29iMVg== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/logger" "0.4.4" + "@firebase/performance" "0.7.2" + "@firebase/performance-types" "0.2.3" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/performance-types@0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.2.3.tgz#5ce64e90fa20ab5561f8b62a305010cf9fab86fb" + integrity sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ== + +"@firebase/performance@0.7.2": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.7.2.tgz#e30ee3e3c120c53f48bde7bdd915687f1e3b27e1" + integrity sha512-DXLLp0R0jdxH/yTmv+WTkOzsLl8YYecXh4lGZE0dzqC0IV8k+AxpLSSWvOTCkAETze8yEU/iF+PtgYVlGjfMMQ== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/installations" "0.6.13" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + web-vitals "^4.2.4" + +"@firebase/remote-config-compat@0.2.13": + version "0.2.13" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-compat/-/remote-config-compat-0.2.13.tgz#4a35c2c6bb582d96aecc45da18f5094359cb5361" + integrity sha512-UmHoO7TxAEJPIZf8e1Hy6CeFGMeyjqSCpgoBkQZYXFI2JHhzxIyDpr8jVKJJN1dmAePKZ5EX7dC13CmcdTOl7Q== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/logger" "0.4.4" + "@firebase/remote-config" "0.6.0" + "@firebase/remote-config-types" "0.4.0" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/remote-config-types@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz#91b9a836d5ca30ced68c1516163b281fbb544537" + integrity sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg== + +"@firebase/remote-config@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.6.0.tgz#24a6966a4d092260983ba8597cc039f5795cd35f" + integrity sha512-Yrk4l5+6FJLPHC6irNHMzgTtJ3NfHXlAXVChCBdNFtgmzyGmufNs/sr8oA0auEfIJ5VpXCaThRh3P4OdQxiAlQ== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/installations" "0.6.13" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/storage-compat@0.3.17": + version "0.3.17" + resolved "https://registry.yarnpkg.com/@firebase/storage-compat/-/storage-compat-0.3.17.tgz#67f6bbc498971e6e404e0ea660fa5df81c5ba5ea" + integrity sha512-CBlODWEZ5b6MJWVh21VZioxwxNwVfPA9CAdsk+ZgVocJQQbE2oDW1XJoRcgthRY1HOitgbn4cVrM+NlQtuUYhw== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/storage" "0.13.7" + "@firebase/storage-types" "0.8.3" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/storage-types@0.8.3": + version "0.8.3" + resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.8.3.tgz#2531ef593a3452fc12c59117195d6485c6632d3d" + integrity sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg== + +"@firebase/storage@0.13.7": + version "0.13.7" + resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.13.7.tgz#809fc23685ad9ba8fdfb3bc758e3353867c9e796" + integrity sha512-FkRyc24rK+Y6EaQ1tYFm3TevBnnfSNA0VyTfew2hrYyL/aYfatBg7HOgktUdB4kWMHNA9VoTotzZTGoLuK92wg== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/util@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.11.0.tgz#e74ee2dc260ec4f9e75fe5d52bc4b0254d9872a9" + integrity sha512-PzSrhIr++KI6y4P6C/IdgBNMkEx0Ex6554/cYd0Hm+ovyFSJtJXqb/3OSIdnBoa2cpwZT1/GW56EmRc5qEc5fQ== + dependencies: + tslib "^2.1.0" + +"@firebase/vertexai@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@firebase/vertexai/-/vertexai-1.2.1.tgz#6962b8b389f10b58033b8c700b27d0cfdc9ebd22" + integrity sha512-cukZ5ne2RsOWB4PB1EO6nTXgOLxPMKDJfEn+XnSV5ZKWM0ID5o0DvbyS59XihFaBzmy2SwJldP5ap7/xUnW4jA== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/component" "0.6.13" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.0" + tslib "^2.1.0" + +"@firebase/webchannel-wrapper@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.3.tgz#a73bab8eb491d7b8b7be2f0e6c310647835afe83" + integrity sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ== + "@floating-ui/core@^1.7.1": version "1.7.1" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.1.tgz#1abc6b157d4a936174f9dbd078278c3a81c8bc6b" @@ -2441,6 +2837,24 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.9.tgz#50dea3616bc8191fb8e112283b49eaff03e78429" integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg== +"@grpc/grpc-js@~1.9.0": + version "1.9.15" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.15.tgz#433d7ac19b1754af690ea650ab72190bd700739b" + integrity sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ== + dependencies: + "@grpc/proto-loader" "^0.7.8" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@^0.7.8": + version "0.7.15" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.15.tgz#4cdfbf35a35461fc843abe8b9e2c0770b5095e60" + integrity sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.5" + yargs "^17.7.2" + "@humanfs/core@^0.19.1": version "0.19.1" resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" @@ -4709,7 +5123,7 @@ deepmerge "^4.3.1" svgo "^3.0.2" -"@svgr/webpack@8.1.0": +"@svgr/webpack@8.1.0", "@svgr/webpack@^8.1.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-8.1.0.tgz#16f1b5346f102f89fda6ec7338b96a701d8be0c2" integrity sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA== @@ -5120,6 +5534,13 @@ dependencies: undici-types "~6.21.0" +"@types/node@>=12.12.47": + version "25.6.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.0.tgz#4e09bad9b469871f2d0f68140198cbd714f4edca" + integrity sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ== + dependencies: + undici-types "~7.19.0" + "@types/node@^11.9.4": version "11.15.54" resolved "https://registry.yarnpkg.com/@types/node/-/node-11.15.54.tgz#59ed60e7b0d56905a654292e8d73275034eb6283" @@ -6346,7 +6767,7 @@ babel-jest@^29.7.0: graceful-fs "^4.2.9" slash "^3.0.0" -babel-loader@8.4.1: +babel-loader@8.4.1, babel-loader@^8.0.0-beta.4, babel-loader@^8.2.4: version "8.4.1" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.4.1.tgz#6ccb75c66e62c3b144e1c5f2eaec5b8f6c08c675" integrity sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA== @@ -7256,7 +7677,7 @@ clean-webpack-plugin@3.0.0: "@types/webpack" "^4.4.31" del "^4.1.1" -clean-webpack-plugin@4.0.0: +clean-webpack-plugin@4.0.0, clean-webpack-plugin@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz#72947d4403d452f38ed61a9ff0ada8122aacd729" integrity sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w== @@ -7717,7 +8138,7 @@ cookie@0.7.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== -copy-webpack-plugin@10.2.4: +copy-webpack-plugin@10.2.4, copy-webpack-plugin@^10.2.0: version "10.2.4" resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz#6c854be3fdaae22025da34b9112ccf81c63308fe" integrity sha512-xFVltahqlsRcyyJqQbDY6EYTtyQZF9rf+JPjwHObLdPFMEISqkFkr7mFoVOC6BfYS/dNThyoQKvziugm+OnwBg== @@ -7887,7 +8308,7 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-env@7.0.3: +cross-env@7.0.3, cross-env@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== @@ -8989,6 +9410,11 @@ dotenv@8.6.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== +dotenv@^14.1.0: + version "14.3.2" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-14.3.2.tgz#7c30b3a5f777c79a3429cb2db358eef6751e8369" + integrity sha512-vwEppIphpFdvaMCaHfCEv9IgwcxMljMw2TnAQBB4VWPvzXQLTb82jwmdOKzlEVUL3gNFT4l4TPKO+Bn+sqcrVQ== + dotenv@^16.4.5: version "16.6.1" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" @@ -10020,7 +10446,7 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -faye-websocket@^0.11.3: +faye-websocket@0.11.4, faye-websocket@^0.11.3: version "0.11.4" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== @@ -10199,6 +10625,40 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +firebase@11.6.0: + version "11.6.0" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-11.6.0.tgz#0a0d76144decc988812cba0bea5e62588ba69fc3" + integrity sha512-Xqm6j6zszIEmI5nW1MPR8yTafoRTSrW3mWG9Lk9elCJtQDQSiTEkKZiNtUm9y6XfOPl8xoF1TNpxZe8HjgA0Og== + dependencies: + "@firebase/analytics" "0.10.12" + "@firebase/analytics-compat" "0.2.18" + "@firebase/app" "0.11.4" + "@firebase/app-check" "0.8.13" + "@firebase/app-check-compat" "0.3.20" + "@firebase/app-compat" "0.2.53" + "@firebase/app-types" "0.9.3" + "@firebase/auth" "1.10.0" + "@firebase/auth-compat" "0.5.20" + "@firebase/data-connect" "0.3.3" + "@firebase/database" "1.0.14" + "@firebase/database-compat" "2.0.5" + "@firebase/firestore" "4.7.10" + "@firebase/firestore-compat" "0.3.45" + "@firebase/functions" "0.12.3" + "@firebase/functions-compat" "0.3.20" + "@firebase/installations" "0.6.13" + "@firebase/installations-compat" "0.2.13" + "@firebase/messaging" "0.12.17" + "@firebase/messaging-compat" "0.2.17" + "@firebase/performance" "0.7.2" + "@firebase/performance-compat" "0.2.15" + "@firebase/remote-config" "0.6.0" + "@firebase/remote-config-compat" "0.2.13" + "@firebase/storage" "0.13.7" + "@firebase/storage-compat" "0.3.17" + "@firebase/util" "1.11.0" + "@firebase/vertexai" "1.2.1" + flat-cache@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" @@ -11295,7 +11755,7 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -idb@^7.0.1: +idb@7.1.1, idb@^7.0.1: version "7.1.1" resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== @@ -13329,6 +13789,11 @@ lodash-es@4.18.1, lodash-es@^4.17.15: resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.18.1.tgz#b962eeb80d9d983a900bf342961fb7418ca10b1d" integrity sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A== +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + lodash.clonedeep@4.5.0, lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -16457,6 +16922,24 @@ protobufjs@^7.1.2, protobufjs@^7.2.4: "@types/node" ">=13.7.0" long "^5.0.0" +protobufjs@^7.2.5: + version "7.5.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.4.tgz#885d31fe9c4b37f25d1bb600da30b1c5b37d286a" + integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + protocol-buffers-schema@^3.3.1: version "3.6.0" resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03" @@ -19207,6 +19690,11 @@ undici-types@~6.21.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== +undici-types@~7.19.0: + version "7.19.2" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.19.2.tgz#1b67fc26d0f157a0cba3a58a5b5c1e2276b8ba2a" + integrity sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg== + undici-types@~7.8.0: version "7.8.0" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.8.0.tgz#de00b85b710c54122e44fbfd911f8d70174cd294" @@ -19646,6 +20134,11 @@ web-streams-polyfill@^3.0.3: resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== +web-vitals@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.4.tgz#1d20bc8590a37769bd0902b289550936069184b7" + integrity sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw== + web-worker@^1.2.0: version "1.5.0" resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.5.0.tgz#71b2b0fbcc4293e8f0aa4f6b8a3ffebff733dcc5" @@ -19684,7 +20177,7 @@ webpack-bundle-analyzer@4.10.2: sirv "^2.0.3" ws "^7.3.1" -webpack-cli@5.1.4: +webpack-cli@5.1.4, webpack-cli@^5.0.2: version "5.1.4" resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b" integrity sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg== @@ -19780,7 +20273,7 @@ webpack-sources@^3.3.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723" integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg== -webpack@5.105.0: +webpack@5.105.0, webpack@5.89.0: version "5.105.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.0.tgz#38b5e6c5db8cbe81debbd16e089335ada05ea23a" integrity sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw== @@ -20352,7 +20845,7 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.3: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs@17.7.2, yargs@^17.3.1, yargs@^17.6.2: +yargs@17.7.2, yargs@^17.3.1, yargs@^17.6.2, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==