ui(components): Adds InputNumber component and 4d panel updates (#4874)

Co-authored-by: sedghi <ar.sedghi@gmail.com>
This commit is contained in:
Dan Rukas 2025-04-01 09:30:32 -04:00 committed by GitHub
parent c4045251fb
commit b7419fb6a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1620 additions and 304 deletions

281
bun.lock
View File

@ -33,7 +33,7 @@
}, },
"addOns/externals/devDependencies": { "addOns/externals/devDependencies": {
"name": "@externals/devDependencies", "name": "@externals/devDependencies",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@kitware/vtk.js": "32.12.0", "@kitware/vtk.js": "32.12.0",
@ -121,14 +121,14 @@
}, },
"addOns/externals/dicom-microscopy-viewer": { "addOns/externals/dicom-microscopy-viewer": {
"name": "@externals/dicom-microscopy-viewer", "name": "@externals/dicom-microscopy-viewer",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"dicom-microscopy-viewer": "^0.46.1", "dicom-microscopy-viewer": "^0.46.1",
}, },
}, },
"extensions/cornerstone": { "extensions/cornerstone": {
"name": "@ohif/extension-cornerstone", "name": "@ohif/extension-cornerstone",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.17", "@cornerstonejs/adapters": "^3.7.17",
@ -153,8 +153,8 @@
"@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.7.17", "@cornerstonejs/dicom-image-loader": "^3.7.17",
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/ui": "3.10.0-beta.140", "@ohif/ui": "3.10.0-beta.142",
"dcmjs": "*", "dcmjs": "*",
"dicom-parser": "^1.8.21", "dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -166,7 +166,7 @@
}, },
"extensions/cornerstone-dicom-pmap": { "extensions/cornerstone-dicom-pmap": {
"name": "@ohif/extension-cornerstone-dicom-pmap", "name": "@ohif/extension-cornerstone-dicom-pmap",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.17", "@cornerstonejs/adapters": "^3.7.17",
@ -175,10 +175,10 @@
"react-color": "^2.19.3", "react-color": "^2.19.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/extension-cornerstone": "3.10.0-beta.140", "@ohif/extension-cornerstone": "3.10.0-beta.142",
"@ohif/extension-default": "3.10.0-beta.140", "@ohif/extension-default": "3.10.0-beta.142",
"@ohif/i18n": "3.10.0-beta.140", "@ohif/i18n": "3.10.0-beta.142",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -189,16 +189,16 @@
}, },
"extensions/cornerstone-dicom-rt": { "extensions/cornerstone-dicom-rt": {
"name": "@ohif/extension-cornerstone-dicom-rt", "name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"react-color": "^2.19.3", "react-color": "^2.19.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/extension-cornerstone": "3.10.0-beta.140", "@ohif/extension-cornerstone": "3.10.0-beta.142",
"@ohif/extension-default": "3.10.0-beta.140", "@ohif/extension-default": "3.10.0-beta.142",
"@ohif/i18n": "3.10.0-beta.140", "@ohif/i18n": "3.10.0-beta.142",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -209,7 +209,7 @@
}, },
"extensions/cornerstone-dicom-seg": { "extensions/cornerstone-dicom-seg": {
"name": "@ohif/extension-cornerstone-dicom-seg", "name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.17", "@cornerstonejs/adapters": "^3.7.17",
@ -218,10 +218,10 @@
"react-color": "^2.19.3", "react-color": "^2.19.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/extension-cornerstone": "3.10.0-beta.140", "@ohif/extension-cornerstone": "3.10.0-beta.142",
"@ohif/extension-default": "3.10.0-beta.140", "@ohif/extension-default": "3.10.0-beta.142",
"@ohif/i18n": "3.10.0-beta.140", "@ohif/i18n": "3.10.0-beta.142",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -232,7 +232,7 @@
}, },
"extensions/cornerstone-dicom-sr": { "extensions/cornerstone-dicom-sr": {
"name": "@ohif/extension-cornerstone-dicom-sr", "name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.17", "@cornerstonejs/adapters": "^3.7.17",
@ -241,10 +241,10 @@
"classnames": "^2.3.2", "classnames": "^2.3.2",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/extension-cornerstone": "3.10.0-beta.140", "@ohif/extension-cornerstone": "3.10.0-beta.142",
"@ohif/extension-measurement-tracking": "3.10.0-beta.140", "@ohif/extension-measurement-tracking": "3.10.0-beta.142",
"@ohif/ui": "3.10.0-beta.140", "@ohif/ui": "3.10.0-beta.142",
"dcmjs": "*", "dcmjs": "*",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -254,7 +254,7 @@
}, },
"extensions/cornerstone-dynamic-volume": { "extensions/cornerstone-dynamic-volume": {
"name": "@ohif/extension-cornerstone-dynamic-volume", "name": "@ohif/extension-cornerstone-dynamic-volume",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.7.17", "@cornerstonejs/core": "^3.7.17",
@ -262,11 +262,11 @@
"classnames": "^2.3.2", "classnames": "^2.3.2",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/extension-cornerstone": "3.10.0-beta.140", "@ohif/extension-cornerstone": "3.10.0-beta.142",
"@ohif/extension-default": "3.10.0-beta.140", "@ohif/extension-default": "3.10.0-beta.142",
"@ohif/i18n": "3.10.0-beta.140", "@ohif/i18n": "3.10.0-beta.142",
"@ohif/ui": "3.10.0-beta.140", "@ohif/ui": "3.10.0-beta.142",
"dcmjs": "*", "dcmjs": "*",
"dicom-parser": "^1.8.21", "dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -276,7 +276,7 @@
}, },
"extensions/default": { "extensions/default": {
"name": "@ohif/extension-default", "name": "@ohif/extension-default",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/calculate-suv": "^1.1.0", "@cornerstonejs/calculate-suv": "^1.1.0",
@ -284,8 +284,8 @@
"lodash.uniqby": "^4.7.0", "lodash.uniqby": "^4.7.0",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/i18n": "3.10.0-beta.140", "@ohif/i18n": "3.10.0-beta.142",
"dcmjs": "*", "dcmjs": "*",
"dicomweb-client": "^0.10.4", "dicomweb-client": "^0.10.4",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
@ -299,7 +299,7 @@
}, },
"extensions/dicom-microscopy": { "extensions/dicom-microscopy": {
"name": "@ohif/extension-dicom-microscopy", "name": "@ohif/extension-dicom-microscopy",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-charls": "^1.2.3",
@ -310,10 +310,10 @@
"mathjs": "^12.4.2", "mathjs": "^12.4.2",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/extension-default": "3.10.0-beta.140", "@ohif/extension-default": "3.10.0-beta.142",
"@ohif/i18n": "3.10.0-beta.140", "@ohif/i18n": "3.10.0-beta.142",
"@ohif/ui": "3.10.0-beta.140", "@ohif/ui": "3.10.0-beta.142",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -324,14 +324,14 @@
}, },
"extensions/dicom-pdf": { "extensions/dicom-pdf": {
"name": "@ohif/extension-dicom-pdf", "name": "@ohif/extension-dicom-pdf",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"classnames": "^2.3.2", "classnames": "^2.3.2",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/ui": "3.10.0-beta.140", "@ohif/ui": "3.10.0-beta.142",
"dcmjs": "*", "dcmjs": "*",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -341,14 +341,14 @@
}, },
"extensions/dicom-video": { "extensions/dicom-video": {
"name": "@ohif/extension-dicom-video", "name": "@ohif/extension-dicom-video",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"classnames": "^2.3.2", "classnames": "^2.3.2",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/ui": "3.10.0-beta.140", "@ohif/ui": "3.10.0-beta.142",
"dcmjs": "*", "dcmjs": "*",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -358,20 +358,20 @@
}, },
"extensions/measurement-tracking": { "extensions/measurement-tracking": {
"name": "@ohif/extension-measurement-tracking", "name": "@ohif/extension-measurement-tracking",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@ohif/ui": "3.10.0-beta.140", "@ohif/ui": "3.10.0-beta.142",
"@xstate/react": "^3.2.2", "@xstate/react": "^3.2.2",
"xstate": "^4.10.0", "xstate": "^4.10.0",
}, },
"peerDependencies": { "peerDependencies": {
"@cornerstonejs/core": "^3.7.17", "@cornerstonejs/core": "^3.7.17",
"@cornerstonejs/tools": "^3.7.17", "@cornerstonejs/tools": "^3.7.17",
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
"@ohif/extension-default": "3.10.0-beta.140", "@ohif/extension-default": "3.10.0-beta.142",
"@ohif/ui": "3.10.0-beta.140", "@ohif/ui": "3.10.0-beta.142",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"dcmjs": "*", "dcmjs": "*",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
@ -384,14 +384,14 @@
}, },
"extensions/test-extension": { "extensions/test-extension": {
"name": "@ohif/extension-test", "name": "@ohif/extension-test",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"classnames": "^2.3.2", "classnames": "^2.3.2",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/ui": "3.10.0-beta.140", "@ohif/ui": "3.10.0-beta.142",
"dcmjs": "0.38.0", "dcmjs": "0.38.0",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -401,14 +401,14 @@
}, },
"extensions/tmtv": { "extensions/tmtv": {
"name": "@ohif/extension-tmtv", "name": "@ohif/extension-tmtv",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"classnames": "^2.3.2", "classnames": "^2.3.2",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/ui": "3.10.0-beta.140", "@ohif/ui": "3.10.0-beta.142",
"dcmjs": "*", "dcmjs": "*",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -418,7 +418,7 @@
}, },
"modes/basic-dev-mode": { "modes/basic-dev-mode": {
"name": "@ohif/mode-basic-dev-mode", "name": "@ohif/mode-basic-dev-mode",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"i18next": "^17.0.3", "i18next": "^17.0.3",
@ -428,17 +428,17 @@
"webpack-merge": "^5.7.3", "webpack-merge": "^5.7.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/extension-cornerstone": "3.10.0-beta.140", "@ohif/extension-cornerstone": "3.10.0-beta.142",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
"@ohif/extension-default": "3.10.0-beta.140", "@ohif/extension-default": "3.10.0-beta.142",
"@ohif/extension-dicom-pdf": "3.10.0-beta.140", "@ohif/extension-dicom-pdf": "3.10.0-beta.142",
"@ohif/extension-dicom-video": "3.10.0-beta.140", "@ohif/extension-dicom-video": "3.10.0-beta.142",
}, },
}, },
"modes/basic-test-mode": { "modes/basic-test-mode": {
"name": "@ohif/mode-test", "name": "@ohif/mode-test",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"i18next": "^17.0.3", "i18next": "^17.0.3",
@ -448,19 +448,19 @@
"webpack-merge": "^5.7.3", "webpack-merge": "^5.7.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/extension-cornerstone": "3.10.0-beta.140", "@ohif/extension-cornerstone": "3.10.0-beta.142",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
"@ohif/extension-default": "3.10.0-beta.140", "@ohif/extension-default": "3.10.0-beta.142",
"@ohif/extension-dicom-pdf": "3.10.0-beta.140", "@ohif/extension-dicom-pdf": "3.10.0-beta.142",
"@ohif/extension-dicom-video": "3.10.0-beta.140", "@ohif/extension-dicom-video": "3.10.0-beta.142",
"@ohif/extension-measurement-tracking": "3.10.0-beta.140", "@ohif/extension-measurement-tracking": "3.10.0-beta.142",
"@ohif/extension-test": "3.10.0-beta.140", "@ohif/extension-test": "3.10.0-beta.142",
}, },
}, },
"modes/longitudinal": { "modes/longitudinal": {
"name": "@ohif/mode-longitudinal", "name": "@ohif/mode-longitudinal",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"i18next": "^17.0.3", "i18next": "^17.0.3",
@ -470,32 +470,32 @@
"webpack-merge": "^5.7.3", "webpack-merge": "^5.7.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/extension-cornerstone": "3.10.0-beta.140", "@ohif/extension-cornerstone": "3.10.0-beta.142",
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.140", "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.142",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.140", "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.142",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
"@ohif/extension-default": "3.10.0-beta.140", "@ohif/extension-default": "3.10.0-beta.142",
"@ohif/extension-dicom-pdf": "3.10.0-beta.140", "@ohif/extension-dicom-pdf": "3.10.0-beta.142",
"@ohif/extension-dicom-video": "3.10.0-beta.140", "@ohif/extension-dicom-video": "3.10.0-beta.142",
"@ohif/extension-measurement-tracking": "3.10.0-beta.140", "@ohif/extension-measurement-tracking": "3.10.0-beta.142",
}, },
}, },
"modes/microscopy": { "modes/microscopy": {
"name": "@ohif/mode-microscopy", "name": "@ohif/mode-microscopy",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"i18next": "^17.0.3", "i18next": "^17.0.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/extension-dicom-microscopy": "3.10.0-beta.140", "@ohif/extension-dicom-microscopy": "3.10.0-beta.142",
}, },
}, },
"modes/preclinical-4d": { "modes/preclinical-4d": {
"name": "@ohif/mode-preclinical-4d", "name": "@ohif/mode-preclinical-4d",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
}, },
@ -504,17 +504,17 @@
"webpack-merge": "^5.7.3", "webpack-merge": "^5.7.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/extension-cornerstone": "3.10.0-beta.140", "@ohif/extension-cornerstone": "3.10.0-beta.142",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.140", "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.142",
"@ohif/extension-cornerstone-dynamic-volume": "3.10.0-beta.140", "@ohif/extension-cornerstone-dynamic-volume": "3.10.0-beta.142",
"@ohif/extension-default": "3.10.0-beta.140", "@ohif/extension-default": "3.10.0-beta.142",
"@ohif/extension-tmtv": "3.10.0-beta.140", "@ohif/extension-tmtv": "3.10.0-beta.142",
}, },
}, },
"modes/segmentation": { "modes/segmentation": {
"name": "@ohif/mode-segmentation", "name": "@ohif/mode-segmentation",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"i18next": "^17.0.3", "i18next": "^17.0.3",
@ -546,19 +546,19 @@
"webpack-merge": "^5.7.3", "webpack-merge": "^5.7.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/extension-cornerstone": "3.10.0-beta.140", "@ohif/extension-cornerstone": "3.10.0-beta.142",
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.140", "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.142",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.140", "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.142",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
"@ohif/extension-default": "3.10.0-beta.140", "@ohif/extension-default": "3.10.0-beta.142",
"@ohif/extension-dicom-pdf": "3.10.0-beta.140", "@ohif/extension-dicom-pdf": "3.10.0-beta.142",
"@ohif/extension-dicom-video": "3.10.0-beta.140", "@ohif/extension-dicom-video": "3.10.0-beta.142",
}, },
}, },
"modes/tmtv": { "modes/tmtv": {
"name": "@ohif/mode-tmtv", "name": "@ohif/mode-tmtv",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"i18next": "^17.0.3", "i18next": "^17.0.3",
@ -568,18 +568,18 @@
"webpack-merge": "^5.7.3", "webpack-merge": "^5.7.3",
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/extension-cornerstone": "3.10.0-beta.140", "@ohif/extension-cornerstone": "3.10.0-beta.142",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
"@ohif/extension-default": "3.10.0-beta.140", "@ohif/extension-default": "3.10.0-beta.142",
"@ohif/extension-dicom-pdf": "3.10.0-beta.140", "@ohif/extension-dicom-pdf": "3.10.0-beta.142",
"@ohif/extension-dicom-video": "3.10.0-beta.140", "@ohif/extension-dicom-video": "3.10.0-beta.142",
"@ohif/extension-measurement-tracking": "3.10.0-beta.140", "@ohif/extension-measurement-tracking": "3.10.0-beta.142",
}, },
}, },
"platform/app": { "platform/app": {
"name": "@ohif/app", "name": "@ohif/app",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-charls": "^1.2.3",
@ -588,23 +588,23 @@
"@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.7.17", "@cornerstonejs/dicom-image-loader": "^3.7.17",
"@emotion/serialize": "^1.1.3", "@emotion/serialize": "^1.1.3",
"@ohif/core": "3.10.0-beta.140", "@ohif/core": "3.10.0-beta.142",
"@ohif/extension-cornerstone": "3.10.0-beta.140", "@ohif/extension-cornerstone": "3.10.0-beta.142",
"@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.140", "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.142",
"@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.140", "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.142",
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.140", "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.142",
"@ohif/extension-default": "3.10.0-beta.140", "@ohif/extension-default": "3.10.0-beta.142",
"@ohif/extension-dicom-microscopy": "3.10.0-beta.140", "@ohif/extension-dicom-microscopy": "3.10.0-beta.142",
"@ohif/extension-dicom-pdf": "3.10.0-beta.140", "@ohif/extension-dicom-pdf": "3.10.0-beta.142",
"@ohif/extension-dicom-video": "3.10.0-beta.140", "@ohif/extension-dicom-video": "3.10.0-beta.142",
"@ohif/extension-test": "3.10.0-beta.140", "@ohif/extension-test": "3.10.0-beta.142",
"@ohif/i18n": "3.10.0-beta.140", "@ohif/i18n": "3.10.0-beta.142",
"@ohif/mode-basic-dev-mode": "3.10.0-beta.140", "@ohif/mode-basic-dev-mode": "3.10.0-beta.142",
"@ohif/mode-longitudinal": "3.10.0-beta.140", "@ohif/mode-longitudinal": "3.10.0-beta.142",
"@ohif/mode-microscopy": "3.10.0-beta.140", "@ohif/mode-microscopy": "3.10.0-beta.142",
"@ohif/mode-test": "3.10.0-beta.140", "@ohif/mode-test": "3.10.0-beta.142",
"@ohif/ui": "3.10.0-beta.140", "@ohif/ui": "3.10.0-beta.142",
"@ohif/ui-next": "3.10.0-beta.140", "@ohif/ui-next": "3.10.0-beta.142",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"classnames": "^2.3.2", "classnames": "^2.3.2",
@ -645,7 +645,7 @@
}, },
"platform/cli": { "platform/cli": {
"name": "@ohif/cli", "name": "@ohif/cli",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"bin": { "bin": {
"ohif-cli": "src/index.js", "ohif-cli": "src/index.js",
}, },
@ -669,7 +669,7 @@
}, },
"platform/core": { "platform/core": {
"name": "@ohif/core", "name": "@ohif/core",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"dcmjs": "*", "dcmjs": "*",
@ -695,14 +695,14 @@
"@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.7.17", "@cornerstonejs/dicom-image-loader": "^3.7.17",
"@ohif/ui": "3.10.0-beta.140", "@ohif/ui": "3.10.0-beta.142",
"cornerstone-math": "0.1.9", "cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21", "dicom-parser": "^1.8.21",
}, },
}, },
"platform/i18n": { "platform/i18n": {
"name": "@ohif/i18n", "name": "@ohif/i18n",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"i18next-locize-backend": "^2.0.0", "i18next-locize-backend": "^2.0.0",
@ -727,7 +727,7 @@
}, },
"platform/ui": { "platform/ui": {
"name": "@ohif/ui", "name": "@ohif/ui",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@testing-library/react": "^13.1.0", "@testing-library/react": "^13.1.0",
"browser-detect": "^0.2.28", "browser-detect": "^0.2.28",
@ -787,7 +787,7 @@
}, },
"platform/ui-next": { "platform/ui-next": {
"name": "@ohif/ui-next", "name": "@ohif/ui-next",
"version": "3.10.0-beta.140", "version": "3.10.0-beta.142",
"dependencies": { "dependencies": {
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.1",
@ -830,13 +830,16 @@
}, },
}, },
"overrides": { "overrides": {
"trim-newlines": "^5.0.0",
"path-to-regexp": "0.1.12",
"glob-parent": "^6.0.2",
"rollup": "2.79.2",
"commander": "8.3.0", "commander": "8.3.0",
"body-parser": "1.20.3", "path-to-regexp": "0.1.12",
"nth-check": "^2.1.1", "nth-check": "^2.1.1",
"trim-newlines": "^5.0.0",
"glob-parent": "^6.0.2",
"trim": "^1.0.0",
"package-json": "^8.1.0",
"sharp": "^0.32.6",
"rollup": "2.79.2",
"body-parser": "1.20.3",
}, },
"packages": { "packages": {
"@adobe/css-tools": ["@adobe/css-tools@4.4.1", "", {}, "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ=="], "@adobe/css-tools": ["@adobe/css-tools@4.4.1", "", {}, "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ=="],

View File

@ -1,12 +1,24 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Button, PanelSection, ButtonGroup, IconButton, InputNumber } from '@ohif/ui'; import {
import { Icons, Tooltip, TooltipTrigger, TooltipContent, Numeric } from '@ohif/ui-next'; Tabs,
TabsList,
TabsTrigger,
Button,
PanelSection,
Icons,
Tooltip,
TooltipTrigger,
TooltipContent,
Numeric,
} from '@ohif/ui-next';
import { Enums } from '@cornerstonejs/core'; import { Enums } from '@cornerstonejs/core';
const controlClassNames = { // Helper function to safely convert any value to uppercase string
sizeClassName: 'w-[58px] h-[28px]', const toUpperCaseString = value => {
arrowsDirection: 'horizontal', if (value === null || value === undefined) {
labelPosition: 'bottom', return '';
}
return String(value).toUpperCase();
}; };
const Header = ({ title, tooltip }) => ( const Header = ({ title, tooltip }) => (
@ -16,18 +28,18 @@ const Header = ({ title, tooltip }) => (
<span> <span>
<Icons.ByName <Icons.ByName
name="info-link" name="info-link"
className="text-primary-active h-[14px] w-[14px]" className="text-primary h-3 w-3"
/> />
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent <TooltipContent
sideOffset={4} sideOffset={4}
className="bg-primary-dark max-w-xs p-2 text-white" className="max-w-xs"
> >
<div>{tooltip}</div> <div>{tooltip}</div>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<span className="text-aqua-pale text-[11px] uppercase">{title}</span> <span className="text-muted-foreground text-xs uppercase tracking-wide">{title}</span>
</div> </div>
); );
@ -51,112 +63,138 @@ const DynamicVolumeControls = ({
const [computedView, setComputedView] = useState(false); const [computedView, setComputedView] = useState(false);
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM); const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
// Wrapper for onGenerate to handle potential errors
const handleGenerate = () => {
try {
if (typeof onGenerate === 'function') {
onGenerate(computeViewMode);
} else {
console.error('onGenerate is not a function', onGenerate);
}
} catch (error) {
console.error('Error in onGenerate:', error);
}
};
return ( return (
<div className="flex select-none flex-col"> <div className="flex select-none flex-col">
<PanelSection <PanelSection defaultOpen={true}>
title="Controls" <PanelSection.Header>Controls</PanelSection.Header>
childrenClassName="space-y-4 pb-5 px-5" <PanelSection.Content className="bg-muted space-y-4 px-5 pt-2 pb-4">
> <div className="mb-4">
<div className="mt-2"> <Header
<Header title="View"
title="View" tooltip={
tooltip={ 'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume'
'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume' }
} />
/> <Tabs
<ButtonGroup className="mt-2 w-full"> value={computedView ? 'computed' : '4d'}
<button onValueChange={value => {
className="w-1/2" const isComputed = value === 'computed';
onClick={() => { setComputedView(isComputed);
setComputedView(false); if (!isComputed && typeof onDynamicClick === 'function') {
onDynamicClick?.(); onDynamicClick();
}
}} }}
className="my-2 w-full"
> >
4D <TabsList className="w-full">
</button> <TabsTrigger
<button value="4d"
className="w-1/2" className="w-1/2"
onClick={() => { >
setComputedView(true); 4D
}} </TabsTrigger>
> <TabsTrigger
Computed value="computed"
</button> className="w-1/2"
</ButtonGroup> >
</div> Computed
<div> </TabsTrigger>
<DimensionGroupControls </TabsList>
onPlayPauseChange={onPlayPauseChange} </Tabs>
isPlaying={isPlaying}
computedView={computedView}
// fps
fps={fps}
onFpsChange={onFpsChange}
minFps={minFps}
maxFps={maxFps}
//
numDimensionGroups={numDimensionGroups}
onDimensionGroupChange={onDimensionGroupChange}
currentDimensionGroupNumber={currentDimensionGroupNumber}
/>
</div>
<div className={`mt-6 flex flex-col ${computedView ? '' : 'ohif-disabled'}`}>
<Header
title="Computed Operation"
tooltip={
<div>
Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to be
applied to the data set.
<br /> Range Slider: Choose the numeric range of dimension groups within which the
operation will be performed.
<br />
Generate Button: Execute the chosen operation on the specified range of data.
</div>
}
/>
<ButtonGroup
className="mt-2 w-full"
separated={true}
>
<button
className="w-1/2"
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUM)}
>
{Enums.DynamicOperatorType.SUM.toString().toUpperCase()}
</button>
<button
className="w-1/2"
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.AVERAGE)}
>
{Enums.DynamicOperatorType.AVERAGE.toString().toUpperCase()}
</button>
<button
className="w-1/2"
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUBTRACT)}
>
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
</button>
</ButtonGroup>
<div className="mt-2 w-full">
<Numeric.Container
mode="doubleRange"
min={1}
max={numDimensionGroups}
values={rangeValues}
onChange={onDoubleRangeChange}
>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
</div> </div>
<Button <div>
className="mt-2 !h-[26px] !w-[115px] self-start !p-0" <DimensionGroupControls
onClick={() => { onPlayPauseChange={onPlayPauseChange}
onGenerate(computeViewMode); isPlaying={isPlaying}
}} computedView={computedView}
> // fps
Generate fps={fps}
</Button> onFpsChange={onFpsChange}
</div> minFps={minFps}
maxFps={maxFps}
//
numDimensionGroups={numDimensionGroups}
onDimensionGroupChange={onDimensionGroupChange}
currentDimensionGroupNumber={currentDimensionGroupNumber}
/>
</div>
<div className={`mt-3 flex flex-col ${computedView ? '' : 'ohif-disabled'}`}>
<Header
title="Computed Operation"
tooltip={
<div>
Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to
be applied to the data set.
<br /> Range Slider: Choose the numeric range of dimension groups within which the
operation will be performed.
<br />
Generate Button: Execute the chosen operation on the specified range of data.
</div>
}
/>
<Tabs
value={String(computeViewMode)}
onValueChange={value => {
setComputeViewMode(value);
}}
className="mt-2 w-full"
>
<TabsList className="w-full gap-1">
{' '}
<TabsTrigger
value={String(Enums.DynamicOperatorType.SUM)}
className="w-1/3"
>
{toUpperCaseString(Enums.DynamicOperatorType.SUM)}
</TabsTrigger>
<TabsTrigger
value={String(Enums.DynamicOperatorType.AVERAGE)}
className="w-1/3"
>
{toUpperCaseString(Enums.DynamicOperatorType.AVERAGE)}
</TabsTrigger>
<TabsTrigger
value={String(Enums.DynamicOperatorType.SUBTRACT)}
className="w-1/3"
>
{toUpperCaseString(Enums.DynamicOperatorType.SUBTRACT)}
</TabsTrigger>
</TabsList>
</Tabs>
<div className="mt-2 w-full">
<Numeric.Container
mode="doubleRange"
min={1}
max={numDimensionGroups || 1}
values={rangeValues || [1, numDimensionGroups || 1]}
onChange={onDoubleRangeChange || (() => {})}
>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
</div>
<Button
variant="default"
size="sm"
className="mt-2 h-[26px] w-[115px] self-start p-0"
onClick={handleGenerate}
>
Generate
</Button>
</div>
</PanelSection.Content>
</PanelSection> </PanelSection>
</div> </div>
); );
@ -179,7 +217,7 @@ function DimensionGroupControls({
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play'); const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
return ( return (
<div className={computedView && 'ohif-disabled'}> <div className={computedView ? 'ohif-disabled' : ''}>
<Header <Header
title="4D Controls" title="4D Controls"
tooltip={ tooltip={
@ -192,32 +230,56 @@ function DimensionGroupControls({
} }
/> />
<div className="mt-3 flex justify-between"> <div className="mt-3 flex justify-between">
<IconButton <Button
id="play-pause-button" id="play-pause-button"
className="bg-customblue-30 h-[26px] w-[58px] rounded-[4px]" variant="secondary"
onClick={() => onPlayPauseChange(!isPlaying)} size="default"
className="w-[58px]"
onClick={() => {
if (typeof onPlayPauseChange === 'function') {
onPlayPauseChange(!isPlaying);
}
}}
> >
<Icons.ByName <Icons.ByName
name={getPlayPauseIconName()} name={getPlayPauseIconName()}
className="active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white" className="text-foreground h-[24px] w-[24px]"
/> />
</IconButton> </Button>
<InputNumber
value={currentDimensionGroupNumber} <Numeric.Container
onChange={onDimensionGroupChange} mode="stepper"
minValue={1} value={currentDimensionGroupNumber || 1}
maxValue={numDimensionGroups} onChange={onDimensionGroupChange || (() => {})}
label="Group" min={1}
{...controlClassNames} max={numDimensionGroups || 1}
/> step={1}
<InputNumber >
value={fps} <div className="flex flex-col items-center">
onChange={onFpsChange} <Numeric.NumberStepper
minValue={minFps} className="h-[28px] w-[58px]"
maxValue={maxFps} direction="horizontal"
{...controlClassNames} />
label="FPS" <Numeric.Label className="text-muted-foreground mt-1 text-sm">Frame</Numeric.Label>
/> </div>
</Numeric.Container>
<Numeric.Container
mode="stepper"
value={fps || 1}
onChange={onFpsChange || (() => {})}
min={minFps || 1}
max={maxFps || 30}
step={1}
>
<div className="flex flex-col items-center">
<Numeric.NumberStepper
className="h-[28px] w-[58px]"
direction="horizontal"
/>
<Numeric.Label className="text-muted-foreground mt-1 text-sm">FPS</Numeric.Label>
</div>
</Numeric.Container>
</div> </div>
</div> </div>
); );

View File

@ -1,7 +1,7 @@
import React, { useEffect, useCallback, useState, ReactElement, useMemo } from 'react'; import React, { useEffect, useCallback, useState, ReactElement, useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import { PanelSection, WindowLevel } from '@ohif/ui'; import { PanelSection, WindowLevel } from '@ohif/ui-next';
import { Enums, eventTarget } from '@cornerstonejs/core'; import { Enums, eventTarget } from '@cornerstonejs/core';
import { useActiveViewportDisplaySets } from '@ohif/core'; import { useActiveViewportDisplaySets } from '@ohif/core';
import { import {
@ -20,7 +20,7 @@ const ViewportWindowLevel = ({
viewportId: string; viewportId: string;
}>): ReactElement => { }>): ReactElement => {
const { cornerstoneViewportService } = servicesManager.services; const { cornerstoneViewportService } = servicesManager.services;
const [windowLevels, setWindowLevels] = useState([]); const [windowLevels, setWindowLevels] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const displaySets = useActiveViewportDisplaySets({ servicesManager }); const displaySets = useActiveViewportDisplaySets({ servicesManager });
@ -200,29 +200,41 @@ const ViewportWindowLevel = ({
}, [viewportId, cornerstoneViewportService, updateViewportHistograms, displaySetIds, isLoading]); }, [viewportId, cornerstoneViewportService, updateViewportHistograms, displaySetIds, isLoading]);
return ( return (
<PanelSection title="Window Level"> <PanelSection defaultOpen={true}>
{windowLevels.map((windowLevel, i) => { <PanelSection.Header>Window Level</PanelSection.Header>
if (!windowLevel.histogram) { <PanelSection.Content className="bg-muted py-1">
return null; {windowLevels.map((windowLevel, i) => {
} if (!windowLevel.histogram) {
return null;
}
return ( return (
<WindowLevel <WindowLevel
key={windowLevel.volumeId} key={windowLevel.volumeId}
title={`${windowLevel.modality}`} histogram={windowLevel.histogram}
histogram={windowLevel.histogram} voi={windowLevel.voi}
voi={windowLevel.voi} step={windowLevel.step}
step={windowLevel.step} showOpacitySlider={windowLevel.showOpacitySlider}
showOpacitySlider={windowLevel.showOpacitySlider} colormap={windowLevel.colormap}
colormap={windowLevel.colormap} onVOIChange={voi => handleVOIChange(windowLevel.volumeId, voi)}
onVOIChange={voi => handleVOIChange(windowLevel.volumeId, voi)} opacity={windowLevel.opacity}
opacity={windowLevel.opacity} onOpacityChange={opacity =>
onOpacityChange={opacity => handleOpacityChange(windowLevel.viewportId, i, windowLevel.volumeId, opacity)
handleOpacityChange(windowLevel.viewportId, i, windowLevel.volumeId, opacity) }
} />
/> );
); })}
})} {windowLevels.length === 0 && !isLoading && (
<div className="text-muted-foreground py-2 text-center text-sm">
No window level data available
</div>
)}
{isLoading && (
<div className="text-muted-foreground py-2 text-center text-sm">
Loading window level data...
</div>
)}
</PanelSection.Content>
</PanelSection> </PanelSection>
); );
}; };

View File

@ -60,6 +60,7 @@
"@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.2",
"@radix-ui/react-hover-card":"^1.1.6",
"@svgr/webpack": "^5.5.0", "@svgr/webpack": "^5.5.0",
"@types/react": "^18.2.29", "@types/react": "^18.2.29",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",

View File

@ -9,6 +9,7 @@ import ShowcaseRow from './ShowcaseRow';
export default function NumericShowcase() { export default function NumericShowcase() {
const [controlledValue, setControlledValue] = useState(0); const [controlledValue, setControlledValue] = useState(0);
const [controlledValues, setControlledValues] = useState([0, 100] as [number, number]); const [controlledValues, setControlledValues] = useState([0, 100] as [number, number]);
const [dimensionGroupNumber, setDimensionGroupNumber] = useState(1);
return ( return (
<div className="space-y-8"> <div className="space-y-8">
@ -87,6 +88,108 @@ export default function NumericShowcase() {
</div> </div>
</ShowcaseRow> </ShowcaseRow>
{/* Stepper Controls */}
<ShowcaseRow
title="Numeric - Stepper Controls"
description="Number input with increment/decrement controls in horizontal or vertical layout"
code={`
// For controlled component
const [dimensionGroupNumber, setDimensionGroupNumber] = useState(1);
<Numeric.Container
mode="stepper"
value={dimensionGroupNumber}
onChange={val => setDimensionGroupNumber(val as number)}
min={1}
max={5}
step={1}
>
<div className="flex flex-col items-center">
<Numeric.NumberStepper className="w-[58px]" direction="horizontal"/>
<Numeric.Label className="mt-1">Frame</Numeric.Label>
</div>
</Numeric.Container>
<Numeric.Container
mode="stepper"
min={0}
max={100}
step={1}
defaultValue={50}
>
<div className="flex items-center space-x-2">
<Numeric.Label>Opacity</Numeric.Label>
<Numeric.NumberStepper className="w-[58px]" direction="horizontal"/>
</div>
</Numeric.Container>
<Numeric.Container
mode="stepper"
min={-10}
max={10}
step={0.1}
defaultValue={0}
>
<div className="flex items-center space-x-2">
<Numeric.Label>Zoom:</Numeric.Label>
<Numeric.NumberStepper className="w-[65px]" direction="vertical"/>
</div>
</Numeric.Container>`}
>
<div className="bg-popover flex w-[300px] flex-col space-y-4 rounded p-4">
<Numeric.Container
mode="stepper"
value={dimensionGroupNumber}
onChange={val => setDimensionGroupNumber(val as number)}
min={1}
max={5}
step={1}
>
<div className="flex flex-col items-center">
<Numeric.NumberStepper
className="flex w-[78px]"
direction="horizontal"
>
<span className="text-muted-foreground text-xs">FPS</span>
</Numeric.NumberStepper>
<Numeric.Label className="mt-1">Frame</Numeric.Label>
</div>
</Numeric.Container>
<Numeric.Container
mode="stepper"
min={0}
max={100}
step={1}
defaultValue={50}
>
<div className="flex items-center space-x-2">
<Numeric.Label>Opacity</Numeric.Label>
<Numeric.NumberStepper
className="w-[58px]"
direction="horizontal"
/>
</div>
</Numeric.Container>
<Numeric.Container
mode="stepper"
min={-10}
max={10}
step={0.1}
defaultValue={0}
>
<div className="flex items-center space-x-2">
<Numeric.NumberStepper
className="w-[65px]"
direction="vertical"
/>
<Numeric.Label>Zoom</Numeric.Label>
</div>
</Numeric.Container>
</div>
</ShowcaseRow>
{/* Single Range Slider */} {/* Single Range Slider */}
<ShowcaseRow <ShowcaseRow
title="Numeric - Single Range" title="Numeric - Single Range"
@ -268,6 +371,20 @@ const [controlledValues, setControlledValues] = useState<[number, number]>([0, 1
<Numeric.NumberInput /> <Numeric.NumberInput />
</Numeric.Container> </Numeric.Container>
<Numeric.Container
mode="stepper"
min={-5}
max={5}
step={0.5}
defaultValue={0}
>
<div className="flex items-center justify-between">
<Numeric.Label>Offset</Numeric.Label>
<Numeric.NumberStepper className="w-[58px]">
</Numeric.NumberStepper>
</div>
</Numeric.Container>
<Numeric.Container <Numeric.Container
mode="singleRange" mode="singleRange"
min={0} min={0}
@ -302,6 +419,22 @@ const [controlledValues, setControlledValues] = useState<[number, number]>([0, 1
<Numeric.NumberInput /> <Numeric.NumberInput />
</Numeric.Container> </Numeric.Container>
<Numeric.Container
mode="stepper"
min={-5}
max={5}
step={0.5}
defaultValue={0}
>
<div className="flex items-center justify-between">
<Numeric.Label>Offset</Numeric.Label>
<Numeric.NumberStepper
className="w-[58px]"
direction="horizontal"
/>
</div>
</Numeric.Container>
<Numeric.Container <Numeric.Container
mode="singleRange" mode="singleRange"
min={0} min={0}

View File

@ -2527,6 +2527,13 @@
dependencies: dependencies:
"@radix-ui/react-primitive" "2.0.1" "@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-arrow@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz#30c0d574d7bb10eed55cd7007b92d38b03c6b2ab"
integrity sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==
dependencies:
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-checkbox@^1.1.1": "@radix-ui/react-checkbox@^1.1.1":
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz#0e2ab913fddf3c88603625f7a9457d73882c8a32" resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz#0e2ab913fddf3c88603625f7a9457d73882c8a32"
@ -2611,6 +2618,17 @@
"@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0" "@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-dismissable-layer@1.1.5":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz#96dde2be078c694a621e55e047406c58cd5fe774"
integrity sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-dropdown-menu@^2.1.1": "@radix-ui/react-dropdown-menu@^2.1.1":
version "2.1.4" version "2.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.4.tgz#5e1f453296dd9ae99224a26c36851832d26cf507" resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.4.tgz#5e1f453296dd9ae99224a26c36851832d26cf507"
@ -2638,6 +2656,21 @@
"@radix-ui/react-primitive" "2.0.1" "@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-hover-card@^1.1.6":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.6.tgz#94fb87c047e1bb3bfd70439cf7ee48165ea4efa5"
integrity sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-dismissable-layer" "1.1.5"
"@radix-ui/react-popper" "1.2.2"
"@radix-ui/react-portal" "1.1.4"
"@radix-ui/react-presence" "1.1.2"
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-icons@^1.3.0": "@radix-ui/react-icons@^1.3.0":
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.2.tgz#09be63d178262181aeca5fb7f7bc944b10a7f441" resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.2.tgz#09be63d178262181aeca5fb7f7bc944b10a7f441"
@ -2718,6 +2751,22 @@
"@radix-ui/react-use-size" "1.1.0" "@radix-ui/react-use-size" "1.1.0"
"@radix-ui/rect" "1.1.0" "@radix-ui/rect" "1.1.0"
"@radix-ui/react-popper@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.2.tgz#d2e1ee5a9b24419c5936a1b7f6f472b7b412b029"
integrity sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==
dependencies:
"@floating-ui/react-dom" "^2.0.0"
"@radix-ui/react-arrow" "1.1.2"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-use-rect" "1.1.0"
"@radix-ui/react-use-size" "1.1.0"
"@radix-ui/rect" "1.1.0"
"@radix-ui/react-portal@1.1.3": "@radix-ui/react-portal@1.1.3":
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.3.tgz#b0ea5141103a1671b715481b13440763d2ac4440" resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.3.tgz#b0ea5141103a1671b715481b13440763d2ac4440"
@ -2726,6 +2775,14 @@
"@radix-ui/react-primitive" "2.0.1" "@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-layout-effect" "1.1.0" "@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-portal@1.1.4":
version "1.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.4.tgz#ff5401ff63c8a825c46eea96d3aef66074b8c0c8"
integrity sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==
dependencies:
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-presence@1.1.2": "@radix-ui/react-presence@1.1.2":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc"
@ -2741,6 +2798,13 @@
dependencies: dependencies:
"@radix-ui/react-slot" "1.1.1" "@radix-ui/react-slot" "1.1.1"
"@radix-ui/react-primitive@2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz#ac8b7854d87b0d7af388d058268d9a7eb64ca8ef"
integrity sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==
dependencies:
"@radix-ui/react-slot" "1.1.2"
"@radix-ui/react-roving-focus@1.1.1": "@radix-ui/react-roving-focus@1.1.1":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz#3b3abb1e03646937f28d9ab25e96343667ca6520" resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz#3b3abb1e03646937f28d9ab25e96343667ca6520"
@ -2829,6 +2893,13 @@
dependencies: dependencies:
"@radix-ui/react-compose-refs" "1.1.1" "@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-slot@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.2.tgz#daffff7b2bfe99ade63b5168407680b93c00e1c6"
integrity sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-switch@^1.1.0": "@radix-ui/react-switch@^1.1.0":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.2.tgz#61323f4cccf25bf56c95fceb3b56ce1407bc9aec" resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.2.tgz#61323f4cccf25bf56c95fceb3b56ce1407bc9aec"

View File

@ -0,0 +1,405 @@
import * as React from 'react';
import { cn } from '../../lib/utils';
import { Input } from '../Input/Input';
import { Button } from '../Button/Button';
import { ChevronUp, ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react';
// Calculate decimal places based on step
const getDecimalPlaces = (step: number): number => {
if (Number.isInteger(step)) {
return 0;
}
const stepStr = step.toString();
if (stepStr.includes('.')) {
return stepStr.split('.')[1].length;
}
return 0;
};
// Enhanced context type - includes shared constraints
type InputNumberContextType = {
value: number | string;
setValue: React.Dispatch<React.SetStateAction<number | string>>;
onChange: (value: number) => void;
// Shared constraints that can be used by subcomponents
constraints?: {
min?: number;
max?: number;
step?: number;
disabled?: boolean;
};
};
// Create context
const InputNumberContext = React.createContext<InputNumberContextType | undefined>(undefined);
// Hook to use context
const useInputNumber = () => {
const context = React.useContext(InputNumberContext);
if (!context) {
throw new Error('useInputNumber must be used within an <InputNumber> component');
}
return context;
};
// Simplified root component props
export interface InputNumberProps {
value: number;
onChange: (value: number) => void;
className?: string;
children?: React.ReactNode;
}
// The single top-level InputNumber component - much simpler now
const InputNumber = React.forwardRef<HTMLDivElement, InputNumberProps>(
({ value, onChange, className, children, ...props }, ref) => {
const [inputValue, setInputValue] = React.useState<number | string>(value);
// Update internal state when prop changes
React.useEffect(() => {
setInputValue(value);
}, [value]);
// Context value - only core state
const contextValue = React.useMemo(
() => ({
value: inputValue,
setValue: setInputValue,
onChange,
}),
[inputValue, onChange]
);
return (
<InputNumberContext.Provider value={contextValue}>
<div
ref={ref}
className={cn('flex', className)}
{...props}
>
{children}
</div>
</InputNumberContext.Provider>
);
}
);
InputNumber.displayName = 'InputNumber';
// Input component with its own constraints
export interface InputNumberInputProps
extends Omit<React.ComponentPropsWithoutRef<typeof Input>, 'onChange'> {
min?: number;
max?: number;
step?: number;
}
const InputNumberInput = React.forwardRef<HTMLInputElement, InputNumberInputProps>(
({ className, min, max, step, ...props }, ref) => {
// Get context which may include constraints from parent
const { value, setValue, onChange, constraints = {} } = useInputNumber();
// Use provided props or fall back to parent constraints or defaults
const effectiveMin = min ?? constraints.min ?? 0;
const effectiveMax = max ?? constraints.max ?? 100;
const effectiveStep = step ?? constraints.step ?? 1;
const decimalPlaces = getDecimalPlaces(effectiveStep);
// Format displayed value with proper decimal places
const displayValue = React.useMemo(() => {
if (typeof value === 'string') {
return value;
}
return decimalPlaces > 0 ? value.toFixed(decimalPlaces) : value.toString();
}, [value, decimalPlaces]);
// Handle input change with constraint awareness
const handleInputChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
// Allow empty string, minus sign, or decimal point for flexibility
if (val === '' || val === '-' || val === '.') {
setValue(val);
return;
}
const numValue = Number(val);
if (!isNaN(numValue)) {
setValue(numValue);
// Only call onChange if value is within boundaries
if (numValue >= effectiveMin && numValue <= effectiveMax) {
onChange(numValue);
}
}
},
[effectiveMin, effectiveMax, onChange, setValue]
);
// Handle blur to format and validate
const handleBlur = React.useCallback(() => {
if (typeof value === 'string') {
// Handle empty or partial inputs
if (value === '' || value === '-' || value === '.') {
setValue(effectiveMin);
onChange(effectiveMin);
return;
}
const numValue = parseFloat(value);
if (isNaN(numValue)) {
setValue(effectiveMin);
onChange(effectiveMin);
return;
}
// Constrain value to min/max
const boundedValue = Math.max(effectiveMin, Math.min(effectiveMax, numValue));
setValue(boundedValue);
onChange(boundedValue);
}
}, [value, effectiveMin, effectiveMax, onChange, setValue]);
return (
<Input
ref={ref}
type="text"
value={displayValue}
onChange={handleInputChange}
onBlur={handleBlur}
className={cn(
'h-6 appearance-none border-none p-0 text-center shadow-none focus:border-none focus:outline-none',
className
)}
{...props}
/>
);
}
);
InputNumberInput.displayName = 'InputNumber.Input';
// Label component with position prop
export interface InputNumberLabelProps extends React.HTMLAttributes<HTMLLabelElement> {
position?: 'left' | 'right' | 'top' | 'bottom';
}
const InputNumberLabel = React.forwardRef<HTMLLabelElement, InputNumberLabelProps>(
({ className, position = 'left', children, ...props }, ref) => {
const positionClasses = {
left: 'mr-2',
right: 'ml-2',
top: 'mb-1',
bottom: 'mt-1',
};
return (
<label
ref={ref}
className={cn('text-muted-foreground text-xs', positionClasses[position], className)}
{...props}
>
{children}
</label>
);
}
);
InputNumberLabel.displayName = 'InputNumber.Label';
// Container for horizontal button layout with constraints that are shared via context
export interface InputNumberHorizontalControlsProps extends React.HTMLAttributes<HTMLDivElement> {
min?: number;
max?: number;
step?: number;
disabled?: boolean;
}
const InputNumberHorizontalControls = React.forwardRef<
HTMLDivElement,
InputNumberHorizontalControlsProps
>(({ className, children, min = 0, max = 100, step = 1, disabled = false, ...props }, ref) => {
// Get existing context and enhance it with constraints
const context = useInputNumber();
const { value, onChange } = context;
// Set constraints in context for child components to use
React.useEffect(() => {
context.constraints = { min, max, step, disabled };
}, [context, min, max, step, disabled]);
// Increment function with local constraints
const increment = React.useCallback(() => {
if (disabled) {
return;
}
const currentValue = typeof value === 'string' ? parseFloat(value) || min : value;
const newValue = Math.min(currentValue + step, max);
onChange(newValue);
}, [value, min, max, step, onChange, disabled]);
// Decrement function with local constraints
const decrement = React.useCallback(() => {
if (disabled) {
return;
}
const currentValue = typeof value === 'string' ? parseFloat(value) || min : value;
const newValue = Math.max(currentValue - step, min);
onChange(newValue);
}, [value, min, max, step, onChange, disabled]);
return (
<div
ref={ref}
className={cn('flex flex-row items-center', className)}
{...props}
>
<Button
variant="ghost"
size="icon"
onClick={decrement}
disabled={disabled}
className="text-primary h-6 w-4 cursor-pointer p-0"
>
<ChevronLeft className="h-4 w-4" />
</Button>
{children}
<Button
variant="ghost"
size="icon"
onClick={increment}
disabled={disabled}
className="text-primary h-6 w-4 cursor-pointer p-0"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
);
});
InputNumberHorizontalControls.displayName = 'InputNumber.HorizontalControls';
// Vertical controls with its own constraints
export interface InputNumberVerticalControlsProps extends React.HTMLAttributes<HTMLDivElement> {
min?: number;
max?: number;
step?: number;
disabled?: boolean;
}
const InputNumberVerticalControls = React.forwardRef<
HTMLDivElement,
InputNumberVerticalControlsProps
>(({ className, children, min = 0, max = 100, step = 1, disabled = false, ...props }, ref) => {
// Get existing context and enhance it with constraints
const context = useInputNumber();
const { value, onChange } = context;
// Set constraints in context for child components to use
React.useEffect(() => {
context.constraints = { min, max, step, disabled };
}, [context, min, max, step, disabled]);
// Increment function with local constraints
const increment = React.useCallback(() => {
if (disabled) {
return;
}
const currentValue = typeof value === 'string' ? parseFloat(value) || min : value;
const newValue = Math.min(currentValue + step, max);
onChange(newValue);
}, [value, min, max, step, onChange, disabled]);
// Decrement function with local constraints
const decrement = React.useCallback(() => {
if (disabled) {
return;
}
const currentValue = typeof value === 'string' ? parseFloat(value) || min : value;
const newValue = Math.max(currentValue - step, min);
onChange(newValue);
}, [value, min, max, step, onChange, disabled]);
return (
<div
ref={ref}
className={cn('flex items-center', className)}
{...props}
>
{children}
<div className="ml-1 flex flex-col">
<Button
variant="ghost"
size="icon"
onClick={increment}
disabled={disabled}
className="text-primary h-3 w-5 pr-px"
>
<ChevronUp className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={decrement}
disabled={disabled}
className="text-primary h-3 w-5 pr-px"
>
<ChevronDown className="h-3 w-3" />
</Button>
</div>
</div>
);
});
InputNumberVerticalControls.displayName = 'InputNumber.VerticalControls';
// Container with size prop
const sizesClasses = {
sm: 'w-[45px] h-[28px]',
md: 'w-[58px] h-[28px]',
lg: 'w-[206px] h-[35px]',
};
export interface InputNumberContainerProps extends React.HTMLAttributes<HTMLDivElement> {
size?: 'sm' | 'md' | 'lg';
sizeClassName?: string;
}
const InputNumberContainer = React.forwardRef<HTMLDivElement, InputNumberContainerProps>(
({ className, size = 'md', sizeClassName, children, ...props }, ref) => {
const sizeToUse = sizeClassName || sizesClasses[size];
return (
<div
ref={ref}
className={cn(
'bg-background border-input flex items-center rounded-md border',
sizeToUse,
className
)}
{...props}
>
{children}
</div>
);
}
);
InputNumberContainer.displayName = 'InputNumber.Container';
// Attach subcomponents
Object.assign(InputNumber, {
Input: InputNumberInput,
Label: InputNumberLabel,
Container: InputNumberContainer,
HorizontalControls: InputNumberHorizontalControls,
VerticalControls: InputNumberVerticalControls,
});
export { InputNumber };

View File

@ -0,0 +1,3 @@
import { InputNumber } from './InputNumber';
export { InputNumber };
export type { InputNumberProps } from './InputNumber';

View File

@ -5,9 +5,23 @@ import { cn } from '../../lib/utils';
import { Input } from '../Input/Input'; import { Input } from '../Input/Input';
import { Slider } from '../Slider/Slider'; import { Slider } from '../Slider/Slider';
import { DoubleSlider } from '../DoubleSlider/DoubleSlider'; import { DoubleSlider } from '../DoubleSlider/DoubleSlider';
import { Button } from '../Button/Button';
import { ChevronUp, ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react';
// Calculate decimal places based on step
const getDecimalPlaces = (step: number): number => {
if (Number.isInteger(step)) {
return 0;
}
const stepStr = step.toString();
if (stepStr.includes('.')) {
return stepStr.split('.')[1].length;
}
return 0;
};
interface NumericMetaContextValue { interface NumericMetaContextValue {
mode: 'number' | 'singleRange' | 'doubleRange'; mode: 'number' | 'singleRange' | 'doubleRange' | 'stepper';
singleValue: number; singleValue: number;
doubleValue: [number, number]; doubleValue: [number, number];
setSingleValue: (val: number) => void; setSingleValue: (val: number) => void;
@ -23,7 +37,7 @@ const NumericMetaContext = createContext<NumericMetaContextValue | null>(null);
1) Container 1) Container
---------------------------------------------------------------------------*/ ---------------------------------------------------------------------------*/
interface NumericMetaContainerProps { interface NumericMetaContainerProps {
mode: 'number' | 'singleRange' | 'doubleRange'; mode: 'number' | 'singleRange' | 'doubleRange' | 'stepper';
value?: number; // for controlled single-value usage from parent value?: number; // for controlled single-value usage from parent
defaultValue?: number; // for uncontrolled single-value usage defaultValue?: number; // for uncontrolled single-value usage
values?: [number, number]; // for controlled double-range usage from parent values?: [number, number]; // for controlled double-range usage from parent
@ -57,10 +71,10 @@ function NumericMetaContainer({
// Use useControllableState for both single and double values // Use useControllableState for both single and double values
const [internalSingleValue, setInternalSingleValue] = useControllableState({ const [internalSingleValue, setInternalSingleValue] = useControllableState({
prop: mode === 'number' || mode === 'singleRange' ? value : undefined, prop: mode === 'number' || mode === 'singleRange' || mode === 'stepper' ? value : undefined,
defaultProp: calculatedDefaultValue, defaultProp: calculatedDefaultValue,
onChange: newVal => { onChange: newVal => {
if (mode === 'number' || mode === 'singleRange') { if (mode === 'number' || mode === 'singleRange' || mode === 'stepper') {
onChange?.(newVal); onChange?.(newVal);
} }
}, },
@ -127,7 +141,7 @@ function NumericMetaLabel({ children, showValue, className }: NumericMetaLabelPr
let displayedValue = ''; let displayedValue = '';
let valueClasses = ''; let valueClasses = '';
if (mode === 'number' || mode === 'singleRange') { if (mode === 'number' || mode === 'singleRange' || mode === 'stepper') {
displayedValue = singleValue.toString(); displayedValue = singleValue.toString();
valueClasses = 'w-10'; valueClasses = 'w-10';
} else if (mode === 'doubleRange') { } else if (mode === 'doubleRange') {
@ -291,12 +305,177 @@ function NumberInput({ className }: NumberInputProps) {
); );
} }
/* -------------------------------------------------------------------------
6) NumberStepper sub-component
---------------------------------------------------------------------------*/
interface NumberStepperProps {
className?: string;
children?: React.ReactNode;
direction?: 'horizontal' | 'vertical';
}
// Modified NumberStepper component to properly position left/right controls
function NumberStepper({ className, children, direction }: NumberStepperProps) {
const ctx = useContext(NumericMetaContext);
if (!ctx) {
throw new Error('NumberStepper must be used inside <Numeric.Container>.');
}
const { mode, singleValue, setSingleValue, min, max, step } = ctx;
if (mode !== 'stepper') {
return null;
}
// Calculate decimal places based on step
const decimalPlaces = getDecimalPlaces(step);
// Format displayed value with proper decimal places
const displayValue = React.useMemo(() => {
return decimalPlaces > 0 ? singleValue.toFixed(decimalPlaces) : singleValue.toString();
}, [singleValue, decimalPlaces]);
const handleInputChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
const val = evt.target.value;
// Allow empty string, minus sign, or decimal point for flexibility
if (val === '' || val === '-' || val === '.') {
return;
}
const numValue = Number(val);
if (!isNaN(numValue)) {
setSingleValue(Math.max(min, Math.min(numValue, max)));
}
};
const handleBlur = () => {
// Ensure value is within constraints when input loses focus
const boundedValue = Math.max(min, Math.min(singleValue, max));
if (boundedValue !== singleValue) {
setSingleValue(boundedValue);
}
};
// Check if children is HorizontalControls component
const hasHorizontalControls = direction === 'horizontal';
if (hasHorizontalControls) {
// We'll handle the control positioning ourselves
return (
<div
className={cn(
'bg-background border-input flex h-[28px] items-center overflow-hidden rounded-md border',
className
)}
>
<LeftControl
min={min}
step={step}
value={singleValue}
setValue={setSingleValue}
/>
<Input
type="text"
value={displayValue}
onChange={handleInputChange}
onBlur={handleBlur}
className="h-6 flex-1 appearance-none border-none p-0 text-center shadow-none focus:border-none focus:outline-none"
/>
{children}
<RightControl
max={max}
step={step}
value={singleValue}
setValue={setSingleValue}
/>
</div>
);
}
return (
<div
className={cn(
'bg-background border-input flex items-center overflow-hidden rounded-md border',
className
)}
>
<Input
type="text"
value={displayValue}
onChange={handleInputChange}
onBlur={handleBlur}
className={cn(
'h-6 flex-1 appearance-none border-none p-0 text-center shadow-none focus:border-none focus:outline-none'
)}
/>
<div className="ml-1 flex flex-col">
<Button
variant="ghost"
size="icon"
onClick={() => setSingleValue(singleValue + step)}
disabled={singleValue >= max}
className="text-primary h-3 w-5 pr-px"
>
<ChevronUp className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => setSingleValue(singleValue - step)}
disabled={singleValue <= min}
className="text-primary h-3 w-5 pr-px"
>
<ChevronDown className="h-3 w-3" />
</Button>
</div>
</div>
);
}
// New components for left and right controls
function LeftControl({ min, step, value, setValue }) {
const decrement = useCallback(() => {
const newValue = Math.max(value - step, min);
setValue(newValue);
}, [value, min, step, setValue]);
return (
<Button
variant="ghost"
size="icon"
onClick={decrement}
className="text-primary h-full w-4 cursor-pointer p-0"
>
<ChevronLeft className="h-4 w-4" />
</Button>
);
}
function RightControl({ max, step, value, setValue }) {
const increment = useCallback(() => {
const newValue = Math.min(value + step, max);
setValue(newValue);
}, [value, max, step, setValue]);
return (
<Button
variant="ghost"
size="icon"
onClick={increment}
className="text-primary h-full w-4 cursor-pointer p-0"
>
<ChevronRight className="h-4 w-4" />
</Button>
);
}
export const Numeric = { export const Numeric = {
Container: NumericMetaContainer, Container: NumericMetaContainer,
Label: NumericMetaLabel, Label: NumericMetaLabel,
SingleRange, SingleRange,
DoubleRange, DoubleRange,
NumberInput, NumberInput,
NumberStepper,
}; };
export default Numeric; export default Numeric;

View File

@ -0,0 +1,162 @@
import React, { useEffect, useState, useMemo } from 'react';
import { Numeric } from '../Numeric/Numeric';
import WindowLevelHistogram from './WindowLevelHistogram';
import { VOI, VOIRange, Histogram, Colormap } from './types';
interface WindowLevelProps {
title?: string;
step?: number;
histogram: Histogram;
voi: VOI;
opacity?: number;
showOpacitySlider?: boolean;
colormap?: Colormap;
style?: 'polygon' | 'bars';
fillColor?: string;
lineColor?: string;
containerClassName?: string;
onVOIChange?: (voi: VOI) => void;
onOpacityChange?: (opacity: number) => void;
}
const convertVOItoVOIRange = (voi: VOI): VOIRange => {
return {
min: voi.windowCenter - voi.windowWidth / 2,
max: voi.windowCenter + voi.windowWidth / 2,
};
};
const WindowLevel = ({
title,
step = 1,
histogram,
voi: voiProp,
opacity: opacityProp = 1,
showOpacitySlider = false,
colormap,
style = 'polygon',
fillColor = '#3f3f3f',
lineColor = '#707070',
containerClassName,
onVOIChange,
onOpacityChange,
}: WindowLevelProps) => {
// Define range from histogram
const range = useMemo(
() => ({
min: histogram.range.min,
max: histogram.range.max,
}),
[histogram]
);
// Initialize VOI range from props
const [voiRange, setVOIRange] = useState<VOIRange>(
voiProp
? convertVOItoVOIRange(voiProp)
: {
min: range.min,
max: range.max,
}
);
// Update VOI range when props change
useEffect(() => {
if (voiProp) {
setVOIRange(convertVOItoVOIRange(voiProp));
}
}, [voiProp]);
// Handle VOI range changes (from slider)
const handleVOIRangeChange = (newValues: [number, number]) => {
const [min, max] = newValues;
if (min === voiRange.min && max === voiRange.max) {
return;
}
const windowWidth = max - min;
const windowCenter = min + windowWidth / 2;
setVOIRange({
min,
max,
});
if (onVOIChange) {
onVOIChange({ windowWidth, windowCenter });
}
};
// Handle opacity changes
const handleOpacityChange = (value: number) => {
if (onOpacityChange) {
onOpacityChange(value);
}
};
return (
<div className={`text-foreground max-w-sm p-1 ${containerClassName || ''}`}>
{title && <div className="text-muted-foreground mb-1 text-sm">{title}</div>}
<div className="space-y-2 px-2">
{/* Range Labels */}
<div className="text-muted-foreground flex h-4 text-sm">
<div className="relative h-fit grow">
<span className="absolute left-0 bottom-px leading-3">{range.min}</span>
</div>
<div className="relative h-fit grow text-right">
<span className="absolute right-0 bottom-px leading-3">{range.max}</span>
</div>
</div>
{/* Histogram Visualization */}
<div className="bg-foreground h-11">
<WindowLevelHistogram
range={range}
voiRange={voiRange}
histogram={histogram}
colormap={colormap}
style={style}
fillColor={fillColor}
lineColor={lineColor}
/>
</div>
{/* Window Level Range Slider */}
<Numeric.Container
mode="doubleRange"
min={range.min}
max={range.max}
values={[voiRange.min, voiRange.max]}
step={step}
onChange={values => handleVOIRangeChange(values as [number, number])}
>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
{/* Opacity Slider (optional) */}
{showOpacitySlider && (
<div className="mt-2">
<div className="flex items-center justify-between gap-2 text-base">
<div className="text-xs text-white">Opacity</div>
<div className="flex-1">
<Numeric.Container
mode="singleRange"
min={0}
max={1}
step={0.05}
value={opacityProp}
onChange={value => handleOpacityChange(value as number)}
>
<Numeric.SingleRange showNumberInput />
</Numeric.Container>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default WindowLevel;

View File

@ -0,0 +1,237 @@
import React, { useLayoutEffect, useEffect, useRef } from 'react';
import { Range, VOIRange, Histogram, Colormap } from './types';
const DEFAULT_COLORMAP = {
Name: 'Grayscale',
RGBPoints: [0, 0, 0, 0, 1, 1, 1, 1],
};
const clamp = (value: number, min: number, max: number) => Math.min(Math.max(min, value), max);
const interpolateVec3 = (a: number[], b: number[], t: number) => {
return [a[0] * (1 - t) + b[0] * t, a[1] * (1 - t) + b[1] * t, a[2] * (1 - t) + b[2] * t];
};
const drawBackground = (canvas: HTMLCanvasElement, range: Range, voiRange: VOIRange, colormap: Colormap) => {
const context = canvas.getContext('2d');
if (!context) return;
const { width, height } = canvas;
const windowWidth = voiRange.max - voiRange.min;
const rgbPoints = colormap.RGBPoints;
const colorsCount = rgbPoints.length / 4;
const getColorPoint = (index: number) => {
const offset = 4 * index;
return index < colorsCount
? {
index,
position: rgbPoints[offset],
color: [rgbPoints[offset + 1], rgbPoints[offset + 2], rgbPoints[offset + 3]],
}
: undefined;
};
let leftColorPoint = undefined;
let rightColorPoint = getColorPoint(0);
const incRawPixelValue = (range.max - range.min) / (width - 1);
let rawPixelValue = range.min;
for (let x = 0; x < width; x++) {
const tVoiRange = (rawPixelValue - voiRange.min) / windowWidth;
// Find the color in a linear way (O(n) complexity)
if (rightColorPoint) {
for (let i = rightColorPoint.index; i < colorsCount; i++) {
if (tVoiRange <= rightColorPoint.position) {
break;
}
leftColorPoint = rightColorPoint;
rightColorPoint = getColorPoint(i + 1);
}
}
let normColor;
if (!leftColorPoint) {
normColor = [...rightColorPoint!.color];
} else if (!rightColorPoint) {
normColor = [...leftColorPoint.color];
} else {
const tColorRange =
(tVoiRange - leftColorPoint.position) /
(rightColorPoint.position - leftColorPoint.position);
normColor = interpolateVec3(leftColorPoint.color, rightColorPoint.color, tColorRange);
}
const color = normColor.map(color => clamp(Math.round(color * 255), 0, 255));
context.fillStyle = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
context.fillRect(x, 0, 1, height);
rawPixelValue += incRawPixelValue;
}
};
/* plots the histogram bins as a polygon that traces the centers of each bin */
const drawPolygonHistogram = (
canvas: HTMLCanvasElement,
context: CanvasRenderingContext2D,
histogram: Histogram,
options: { scale: (val: number) => number, fillColor: string, lineColor: string }
) => {
const xScale = canvas.width / histogram.numBins;
const { scale } = options;
const maxVal = scale(histogram.maxBinValue);
context.beginPath();
context.moveTo(0, canvas.height);
context.lineTo(0, canvas.height - (canvas.height * scale(histogram.bins[0])) / maxVal);
let x = xScale / 2;
for (let bin = 0; bin < histogram.numBins; bin++) {
context.lineTo(x, canvas.height - (canvas.height * scale(histogram.bins[bin])) / maxVal);
x += xScale;
}
context.lineTo(
canvas.width,
canvas.height - (canvas.height * scale(histogram.bins[histogram.numBins - 1])) / maxVal
);
context.lineTo(canvas.width, canvas.height);
context.lineTo(0, canvas.height);
context.closePath();
context.fill();
context.stroke();
};
/* plots the histogram bins as a series of n vertical bars (n = number of bins) */
const drawBarHistogram = (
canvas: HTMLCanvasElement,
context: CanvasRenderingContext2D,
histogram: Histogram,
options: { scale: (val: number) => number, fillColor: string, lineColor: string }
) => {
const xScale = canvas.width / histogram.numBins;
const { scale } = options;
const maxVal = scale(histogram.maxBinValue);
context.beginPath();
for (let bin = 0; bin < histogram.numBins; bin++) {
context.moveTo(xScale * bin, canvas.height);
context.lineTo(
xScale * bin,
canvas.height - (canvas.height * scale(histogram.bins[bin])) / maxVal
);
}
context.closePath();
context.strokeStyle = options.fillColor;
context.lineWidth = xScale;
context.stroke();
};
const drawHistogram = (
canvas: HTMLCanvasElement,
histogram: Histogram,
options: {
style: 'polygon' | 'bars',
fillColor: string,
lineColor: string,
scale: (val: number) => number
}
) => {
const context = canvas.getContext('2d');
if (!context) return;
context.fillStyle = options.fillColor;
context.strokeStyle = options.lineColor;
if (options.style === 'polygon') {
drawPolygonHistogram(canvas, context, histogram, options);
} else if (options.style === 'bars') {
drawBarHistogram(canvas, context, histogram, options);
} else {
throw new Error(`Invalid style (${options.style})`);
}
};
interface WindowLevelHistogramProps {
range: Range;
voiRange: VOIRange;
histogram: Histogram;
colormap?: Colormap;
style?: 'polygon' | 'bars';
fillColor?: string;
lineColor?: string;
}
const WindowLevelHistogram = ({
range,
voiRange,
histogram,
colormap = DEFAULT_COLORMAP,
style = 'polygon',
fillColor = '#3f3f3f',
lineColor = '#707070',
}: WindowLevelHistogramProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const minVOIPercent = ((voiRange.min - range.min) / (range.max - range.min)) * 100;
const maxVoiPercent = (1 - (range.max - voiRange.max) / (range.max - range.min)) * 100;
const background =
colormap.Name !== 'Grayscale'
? undefined
: `linear-gradient(to right, #000 0%, #000 ${minVOIPercent}%, #fff ${maxVoiPercent}%, #fff 100%)`;
useLayoutEffect(() => {
const container = containerRef.current;
const canvas = canvasRef.current;
if (!container || !canvas) return;
canvas.width = container.offsetWidth;
canvas.height = container.offsetHeight;
}, [containerRef, canvasRef]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const context = canvas.getContext('2d');
if (!context) return;
const fnIdentity = (x: number) => x;
const options = {
style,
fillColor,
lineColor,
scale: fnIdentity,
};
context.clearRect(0, 0, canvas.width, canvas.height);
drawBackground(canvas, range, voiRange, colormap);
drawHistogram(canvas, histogram, options);
}, [range, voiRange, histogram, colormap, style, fillColor, lineColor]);
return (
<div
ref={containerRef}
className="h-full"
style={{ background }}
>
<canvas
ref={canvasRef}
width="1"
height="1"
></canvas>
</div>
);
};
export default WindowLevelHistogram;

View File

@ -0,0 +1,3 @@
export { default as WindowLevel } from './WindowLevel';
export { default as WindowLevelHistogram } from './WindowLevelHistogram';
export * from './types';

View File

@ -0,0 +1,41 @@
// Window-level related types
export interface Range {
min: number;
max: number;
}
export interface VOIRange {
min: number;
max: number;
}
export interface VOI {
windowWidth: number;
windowCenter: number;
}
export interface Histogram {
bins: number[] | Int32Array;
numBins: number;
maxBin: number;
maxBinValue: number;
range: Range;
}
export interface Colormap {
Name: string;
RGBPoints: number[];
}
export interface WindowLevelData {
volumeId: string;
viewportId: string;
modality: string;
voi: VOI;
step: number;
histogram: Histogram;
showOpacitySlider: boolean;
opacity?: number;
colormap?: Colormap;
}

View File

@ -35,6 +35,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from './Tabs';
import { Toggle, toggleVariants } from './Toggle'; import { Toggle, toggleVariants } from './Toggle';
import { ToggleGroup, ToggleGroupItem } from './ToggleGroup'; import { ToggleGroup, ToggleGroupItem } from './ToggleGroup';
import { Input } from './Input'; import { Input } from './Input';
import { InputNumber } from './InputNumber';
import { Label } from './Label'; import { Label } from './Label';
import { Switch } from './Switch'; import { Switch } from './Switch';
import { Checkbox } from './Checkbox'; import { Checkbox } from './Checkbox';
@ -59,6 +60,7 @@ import { AboutModal, ImageModal, UserPreferencesModal } from './OHIFModals';
import Modal from './Modal/Modal'; import Modal from './Modal/Modal';
import { FooterAction } from './FooterAction'; import { FooterAction } from './FooterAction';
import { InputFilter } from './InputFilter'; import { InputFilter } from './InputFilter';
import { WindowLevel, WindowLevelHistogram } from './OHIFPanels';
import { import {
DropdownMenu, DropdownMenu,
@ -154,11 +156,12 @@ export {
Calendar, Calendar,
DatePickerWithRange, DatePickerWithRange,
Input, Input,
InputNumber,
Label, Label,
Tabs, Tabs,
TabsContent,
TabsList, TabsList,
TabsTrigger, TabsTrigger,
TabsContent,
Separator, Separator,
Switch, Switch,
Checkbox, Checkbox,
@ -245,4 +248,6 @@ export {
FooterAction, FooterAction,
ToolSettings, ToolSettings,
InputFilter, InputFilter,
WindowLevel,
WindowLevelHistogram,
}; };

View File

@ -1,6 +1,5 @@
import * as utils from './utils';
export * from './components'; export * from './components';
export * from './contextProviders'; export * from './contextProviders';
import * as utils from './utils';
export { utils }; export { utils };