feat(segmentation mode): Add create, and export SEG with Brushes (#3632)

This commit is contained in:
Alireza authored and GitHub committed 2023-09-22 10:23:44 -04:00
1 parent 896866e9d0
commit 48bbd6281a
186 files changed
+6801 -2883

No files matched your search

+9 -5
View File
@@ -1,12 +1,12 @@
import path from 'path';
import path, { dirname, join } from 'path';
import remarkGfm from 'remark-gfm';
import type { StorybookConfig } from '@storybook/react-webpack5';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(mdx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
getAbsolutePath('@storybook/addon-links'),
getAbsolutePath('@storybook/addon-essentials'),
// Other addons go here
{
name: '@storybook/addon-docs',
@@ -19,9 +19,9 @@ const config: StorybookConfig = {
},
},
],
core: { builder: '@storybook/builder-webpack5' },
core: {},
framework: {
name: '@storybook/react-webpack5',
name: getAbsolutePath('@storybook/react-webpack5'),
options: {},
},
docs: {
@@ -91,3 +91,7 @@ const config: StorybookConfig = {
};
export default config;
function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, 'package.json')));
}
+10 -9
View File
@@ -23,6 +23,7 @@
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"storybook": "storybook dev -p 6006",
"clean": "rm -rf node_modules/.cache/storybook",
"dev": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
@@ -53,14 +54,14 @@
},
"devDependencies": {
"@babel/core": "^7.21.4",
"@storybook/addon-actions": "^7.0.7",
"@storybook/addon-docs": "^7.0.7",
"@storybook/addon-essentials": "^7.0.7",
"@storybook/addon-links": "^7.0.7",
"@storybook/cli": "^7.0.7",
"@storybook/react": "^7.0.7",
"@storybook/react-webpack5": "^7.0.7",
"@storybook/source-loader": "^7.0.7",
"@storybook/addon-actions": "^7.2.2",
"@storybook/addon-docs": "^7.2.2",
"@storybook/addon-essentials": "^7.2.2",
"@storybook/addon-links": "^7.2.2",
"@storybook/cli": "^7.2.2",
"@storybook/react": "^7.2.2",
"@storybook/react-webpack5": "^7.2.2",
"@storybook/source-loader": "^7.2.2",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.2",
"dotenv-webpack": "^8.0.1",
@@ -68,7 +69,7 @@
"postcss-loader": "^7.2.4",
"prop-types": "^15.8.1",
"remark-gfm": "^3.0.1",
"storybook": "^7.0.7",
"storybook": "^7.2.2",
"tailwindcss": "3.2.4"
}
}
@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill="currentColor" fill-rule="evenodd" d="M10 13L5 7.737 5.7 7 10 11.526 14.3 7 15 7.737z" transform="rotate(90 10 10)"/>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path fill="currentColor" fill-rule="evenodd" stroke="currentColor" d="M10 13L5 7.737 5.7 7 10 11.526 14.3 7 15 7.737z" transform="rotate(90 10 10)"/>
</svg>

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 246 B

+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12.0833333px" height="12.0833333px" viewBox="0 0 12.0833333 12.0833333" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-add</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-78.875, -64.375)">
<g id="icon-add" transform="translate(75, 60)">
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
<g id="Group-18" transform="translate(4.5, 5)" stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25">
<line x1="5.41666667" y1="0" x2="5.41666667" y2="10.8333333" id="Path"></line>
<line x1="10.8333333" y1="5.41666667" x2="0" y2="5.41666667" id="Path"></line>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 961 B

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="10.25px" height="10.25px" viewBox="0 0 10.25 10.25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-delete</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-254.875, -64.875)">
<g id="icon-delete" transform="translate(250, 60)">
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
<g id="Group-31" transform="translate(5, 5)" stroke="#348CFD" stroke-linecap="round" stroke-width="1.25">
<g id="Group-30">
<line x1="0.5" y1="0.5" x2="9.5" y2="9.5" id="Line-4"></line>
<line x1="0.5" y1="0.5" x2="9.5" y2="9.5" id="Line-4" transform="translate(5, 5) scale(-1, 1) translate(-5, -5)"></line>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1019 B

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="8.88756485px" height="5.15088921px" viewBox="0 0 8.88756485 5.15088921" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-disclosure-close</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-115.6464, -67.6464)">
<g id="icon-disclosure-close" transform="translate(110, 60)">
<polyline id="Path-2" stroke="#348CFD" points="6 8 10.090229 12.090229 14.1804581 8"></polyline>
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 737 B

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="5.15088921px" height="8.88756485px" viewBox="0 0 5.15088921 8.88756485" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-disclosure-open</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-152.338, -65.6013)">
<g id="icon-disclosure-open" transform="translate(145, 60)">
<polyline id="Path-2" stroke="#348CFD" transform="translate(10.0902, 10.0451) rotate(90) translate(-10.0902, -10.0451)" points="6 8 10.090229 12.090229 14.1804581 8"></polyline>
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 815 B

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="15.7826087px" height="15.7826087px" viewBox="0 0 15.7826087 15.7826087" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-display-settings</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="Segmentation---assets" transform="translate(-42.127, -62.1087)" stroke="#348CFD">
<g id="icon-display-settings" transform="translate(40, 60)">
<circle id="Oval" cx="10" cy="4.34782609" r="1.73913043"></circle>
<line x1="11.7373913" y1="4.34782609" x2="17.4095652" y2="4.34782609" id="Path"></line>
<line x1="2.62695652" y1="4.34782609" x2="8.2573913" y2="4.34782609" id="Path"></line>
<g id="Group-26" transform="translate(10.0183, 10) scale(-1, 1) translate(-10.0183, -10)translate(2.627, 8.2609)">
<circle id="Oval" transform="translate(3.8948, 1.7391) scale(-1, 1) translate(-3.8948, -1.7391)" cx="3.89478261" cy="1.73913043" r="1.73913043"></circle>
<line x1="5.63043478" y1="1.73913043" x2="14.7826087" y2="1.73913043" id="Path" transform="translate(10.2065, 1.7391) scale(-1, 1) translate(-10.2065, -1.7391)"></line>
<line x1="0" y1="1.73913043" x2="2.15217391" y2="1.73913043" id="Path" transform="translate(1.0761, 1.7391) scale(-1, 1) translate(-1.0761, -1.7391)"></line>
</g>
<circle id="Oval" cx="8.26086957" cy="15.6521739" r="1.73913043"></circle>
<line x1="9.99652174" y1="15.6521739" x2="17.4095652" y2="15.6521739" id="Path"></line>
<line x1="2.62695652" y1="15.6521739" x2="6.52173913" y2="15.6521739" id="Path"></line>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-more-menu</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-286, -61)">
<g id="icon-more-menu" transform="translate(285, 60)">
<g id="Group-28" transform="translate(1, 1)">
<circle id="Oval" fill="#348CFD" cx="5.625" cy="9" r="1.125"></circle>
<circle id="Oval" fill="#348CFD" cx="9" cy="9" r="1.125"></circle>
<circle id="Oval" fill="#348CFD" cx="12.375" cy="9" r="1.125"></circle>
<circle id="Oval" stroke="#348CFD" cx="9" cy="9" r="8.5"></circle>
</g>
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1008 B

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12.8339446px" height="12.73108px" viewBox="0 0 12.8339446 12.73108" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-rename</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-218.9974, -64)">
<g id="icon-rename" transform="translate(215, 60)">
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
<g id="Group-29" transform="translate(5, 5)" stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round">
<path d="M7.62511295,0.871568536 L9.85691246,3.10336805 L2.82291656,10.1381292 L-0.897323283,11.6258043 L0.590351811,7.90556444 L7.62511295,0.871568536 Z" id="Path"></path>
<path d="M9.75273119,-0.5 C10.1567445,-0.5 10.5607578,-0.345874112 10.8690096,-0.0376223372 C11.1772614,0.270629438 11.3313873,0.674642763 11.3313873,1.07865609 C11.3313873,1.48266941 11.1772614,1.88668274 10.8688429,2.19510116 L9.75256454,3.31032741 L7.52105985,1.07882272 L8.63645277,-0.0376223372 C8.94470454,-0.345874112 9.34871787,-0.5 9.75273119,-0.5 Z" id="Path"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="15.4692312px" height="13.3478261px" viewBox="0 0 15.4692312 13.3478261" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-toggle-all-visibility</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-181.8615, -62.9135)">
<g id="icon-toggle-all-visibility" transform="translate(180, 60)">
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
<g id="Group-15" transform="translate(2.4615, 3.5385)" stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round">
<g id="view-off">
<g id="Group-14" opacity="0.718994867" transform="translate(0, 2.0163)" stroke-width="1.2">
<path d="M11.7987926,1.72967893 C12.7139877,2.39263252 13.5437544,3.16613677 14.2692308,4.0326087 C14.2692308,4.0326087 11.0754047,8.06521739 7.13461538,8.06521739 C6.50340139,8.06171444 5.87618966,7.96470569 5.27341137,7.77735117" id="Path"></path>
<path d="M2.46175251,6.32809365 C1.55002525,5.66691798 0.723215132,4.89595088 0,4.0326087 C0,4.0326087 3.19382609,0 7.13461538,0 C7.65800586,0.00220851946 8.17906018,0.0699455819 8.68561873,0.201630435" id="Path"></path>
<path d="M4.65301003,4.0326087 C4.65301003,2.66205591 5.76406259,1.55100334 7.13461538,1.55100334" id="Path"></path>
<path d="M9.61622074,4.0326087 C9.61622074,5.40316149 8.50516818,6.51421405 7.13461538,6.51421405" id="Path"></path>
</g>
<line x1="13.1835284" y1="0" x2="1.08570234" y2="12.0978261" id="Path" stroke-width="1.25"></line>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="23.7493737px" height="24.7494038px" viewBox="0 0 23.7493737 24.7494038" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-tool-brush</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-40.625, -105.125)">
<g id="icon-tool-brush" transform="translate(40, 105)">
<g id="brush" transform="translate(1.5, 1)" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.75">
<path d="M0.285314129,22.3807117 C0.106218646,22.2955642 -0.00563032043,22.1126205 0.000218787638,21.9144007 C0.0060678957,21.716181 0.128509536,21.5401507 0.312314129,21.4657117 C2.69331413,20.5017117 1.92631413,17.7877117 2.63331413,16.3367117 C3.49915231,14.5926292 5.60557215,13.8677677 7.36131413,14.7097117 C12.6773141,17.3017117 6.34931413,25.2667117 0.285314129,22.3807117 Z" id="Path"></path>
<path d="M21.5333141,0.349711738 C20.9877208,-0.135928959 20.1584241,-0.113148488 19.6403141,0.401711738 L7.74931413,12.2607117 C9.32532634,12.7428889 10.6345655,13.8504286 11.3713141,15.3247117 L21.7003141,2.24271174 C22.156142,1.66747397 22.0828125,0.836259875 21.5333141,0.349711738 Z" id="Path"></path>
</g>
<rect id="Rectangle" x="0" y="0" width="25" height="25"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24.25px" height="22.418445px" viewBox="0 0 24.25 22.418445" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-tool-eraser</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-83.375, -106.3752)">
<g id="icon-tool-eraser" transform="translate(83, 105)">
<g id="eraser-1" transform="translate(1, 2)" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25">
<path d="M7,19.1705986 C5.7337971,19.1021794 4.52894862,18.6033051 3.585,17.7565986 L1.414,15.5835986 C0.635868537,14.8011205 0.635868537,13.5370767 1.414,12.7545986 L13.585,0.583598597 C14.3674781,-0.194532866 15.6315219,-0.194532866 16.414,0.583598597 L21.585,5.7545986 C22.3631315,6.53707671 22.3631315,7.80112049 21.585,8.5835986 L12.414,17.7545986 C11.4705925,18.601822 10.2661029,19.1013994 9,19.1705986 L7,19.1705986 Z" id="Path"></path>
<line x1="9" y1="5.1685986" x2="17" y2="13.1685986" id="Path"></line>
<line x1="0" y1="21.1685986" x2="23" y2="21.1685986" id="Path"></line>
</g>
<rect id="Rectangle" x="0" y="0" width="25" height="25"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24.2500949px" height="20.25px" viewBox="0 0 24.2500949 20.25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-tool-scissor</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-126.375, -107.375)">
<g id="icon-tool-scissor" transform="translate(126, 105)">
<rect id="Rectangle" x="0" y="0" width="25" height="25"></rect>
<g id="scissors-2" transform="translate(1, 3)" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25">
<circle id="Oval" cx="3.5" cy="3.5" r="3.5"></circle>
<path d="M10.648,8.232 L23,15.587 C22.3744808,16.5550109 21.1109418,16.8812258 20.095,16.337 L4.154,6.939" id="Path"></path>
<circle id="Oval" cx="3.5" cy="15.5" r="3.5"></circle>
<path d="M12.779,9.501 L23,3.414 C22.3747822,2.44557124 21.1108803,2.11926264 20.095,2.664 L4.154,12.061" id="Path"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24.5px" height="16.5px" viewBox="0 0 24.5 16.5" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-tool-threshold</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-169.25, -109.25)">
<g id="icon-tool-threshold" transform="translate(169, 105)">
<g id="Group-16" transform="translate(1, 5)" stroke-width="1.5">
<g id="Group-13">
<line x1="19.5" y1="12.5" x2="23" y2="12.5" id="Line-2" stroke="currentColor" stroke-linecap="round"></line>
<line x1="7.63278329e-17" y1="12.5" x2="14" y2="12.5" id="Line-2" stroke="currentColor" stroke-linecap="round"></line>
<circle id="Oval" stroke="currentColor" cx="16.5" cy="12.5" r="2.5"></circle>
<line x1="10.5" y1="2.5" x2="22.5" y2="2.5" id="Line-2" stroke="currentColor" stroke-linecap="round"></line>
<line x1="2.27456942e-13" y1="2.5" x2="5.5" y2="2.5" id="Line-2" stroke="currentColor" stroke-linecap="round"></line>
<circle id="Oval" stroke="currentColor" cx="8" cy="2.5" r="2.5"></circle>
</g>
</g>
<rect id="Rectangle" x="0" y="0" width="25" height="25"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+3 -3
View File
@@ -2,9 +2,9 @@
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M0 0h21v21H0z"/>
<path d="M17.29 10.6c-.676-.77-3.378-3.6-6.61-3.6-3.23 0-5.933 2.83-6.609 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="#3D5871" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.68 8.75c.69 0 1.298.344 1.666.87l-2.84 2.84c-.52-.37-.859-.98-.859-1.668 0-1.128.91-2.042 2.033-2.042zM12.689 10.47a2.038 2.038 0 0 1-2.337 2.337z" fill="#3D5871"/>
<g stroke="#3D5871" stroke-linecap="round" stroke-linejoin="round">
<path d="M17.29 10.6c-.676-.77-3.378-3.6-6.61-3.6-3.23 0-5.933 2.83-6.609 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.68 8.75c.69 0 1.298.344 1.666.87l-2.84 2.84c-.52-.37-.859-.98-.859-1.668 0-1.128.91-2.042 2.033-2.042zM12.689 10.47a2.038 2.038 0 0 1-2.337 2.337z" fill="currentColor"/>
<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M16.396 5 5.02 16.375"/>
</g>
</g>

Before

Width:  |  Height:  |  Size: 782 B

After

Width:  |  Height:  |  Size: 797 B

@@ -1,11 +0,0 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M0 0h21v21H0z"/>
<path d="M17.29 10.6c-.676-.77-3.378-3.6-6.61-3.6-3.23 0-5.933 2.83-6.609 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.68 8.75c.69 0 1.298.344 1.666.87l-2.84 2.84c-.52-.37-.859-.98-.859-1.668 0-1.128.91-2.042 2.033-2.042zM12.689 10.47a2.038 2.038 0 0 1-2.337 2.337z" fill="#348CFD"/>
<g stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round">
<path d="M16.396 5 5.02 16.375"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 782 B

@@ -1,7 +1,7 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M8 10.5V8.559A2.554 2.554 0 0 1 10.549 6a2.554 2.554 0 0 1 2.548 2.559v2.483" stroke="#FFF" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.468 11c0-.552-.42-1-.938-1H7.437c-.517 0-.937.448-.937 1v5c0 .552.42 1 .937 1h6.093c.518 0 .938-.448.938-1v-5z" fill="#FFF"/>
<path d="M8 10.5V8.559A2.554 2.554 0 0 1 10.549 6a2.554 2.554 0 0 1 2.548 2.559v2.483" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.468 11c0-.552-.42-1-.938-1H7.437c-.517 0-.937.448-.937 1v5c0 .552.42 1 .937 1h6.093c.518 0 .938-.448.938-1v-5z" stroke="currentColor"/>
</g>
</svg>
@@ -1,9 +0,0 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<g transform="translate(4 7)">
<path d="M13.29 3.6C12.614 2.83 9.912 0 6.68 0 3.45 0 .747 2.83.071 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round"/>
<ellipse fill="#348CFD" cx="6.68" cy="3.792" rx="2.033" ry="2.042"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 558 B

@@ -2,8 +2,8 @@
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<g transform="translate(4 7)">
<path d="M13.29 3.6C12.614 2.83 9.912 0 6.68 0 3.45 0 .747 2.83.071 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="#FFF" stroke-linecap="round" stroke-linejoin="round"/>
<ellipse fill="#FFF" cx="6.68" cy="3.792" rx="2.033" ry="2.042"/>
<path d="M13.29 3.6C12.614 2.83 9.912 0 6.68 0 3.45 0 .747 2.83.071 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<ellipse fill="currentColor" cx="6.68" cy="3.792" rx="2.033" ry="2.042"/>
</g>
</g>
</svg>
@@ -1,11 +0,0 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M0 0h21v21H0z"/>
<path d="M17.29 10.6c-.676-.77-3.378-3.6-6.61-3.6-3.23 0-5.933 2.83-6.609 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="#FFF" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.68 8.75c.69 0 1.298.344 1.666.87l-2.84 2.84c-.52-.37-.859-.98-.859-1.668 0-1.128.91-2.042 2.033-2.042zM12.689 10.47a2.038 2.038 0 0 1-2.337 2.337z" fill="#FFF"/>
<g stroke="#FFF" stroke-linecap="round" stroke-linejoin="round">
<path d="M16.396 5 5.02 16.375"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 773 B

@@ -1,7 +1,7 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M16.014 7.559A2.554 2.554 0 0 0 13.466 5a2.554 2.554 0 0 0-2.549 2.559v2.483" stroke="#FFF" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.968 10.824c0-.553-.42-1-.938-1H5.937c-.517 0-.937.447-.937 1v5c0 .552.42 1 .937 1h6.093c.518 0 .938-.448.938-1v-5z" fill="#FFF"/>
<path d="M16.014 7.559A2.554 2.554 0 0 0 13.466 5a2.554 2.554 0 0 0-2.549 2.559v2.483" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.968 10.824c0-.553-.42-1-.938-1H5.937c-.517 0-.937.447-.937 1v5c0 .552.42 1 .937 1h6.093c.518 0 .938-.448.938-1v-5z" fill="currentColor"/>
</g>
</svg>
@@ -1,3 +1,3 @@
<svg width="6" height="5" viewBox="0 0 6 5" xmlns="http://www.w3.org/2000/svg">
<path d="M3 5 6 .5H0z" fill="#726F7E" fill-rule="evenodd"/>
<path d="M3 5 6 .5H0z" fill="currnetColor" fill-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 151 B

After

Width:  |  Height:  |  Size: 156 B

@@ -0,0 +1,3 @@
<svg width="5" height="6" viewBox="0 0 5 6" xmlns="http://www.w3.org/2000/svg">
<path d="M0.5 3L5 0V6z" fill="currentColor" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 157 B

@@ -0,0 +1,3 @@
<svg width="5" height="6" viewBox="0 0 5 6" xmlns="http://www.w3.org/2000/svg">
<path transform="rotate(90 2.5 2.5)" d="M3 5 6 .5H0z" fill="currentColor" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 187 B

@@ -0,0 +1,64 @@
import React, { useState, useEffect } from 'react';
import classnames from 'classnames';
import { PanelSection, Icon, Tooltip } from '../../components';
import ToolSettings from './ToolSettings';
const AdvancedToolbox = ({ title, items }) => {
const [activeItemName, setActiveItemName] = useState(null);
useEffect(() => {
// see if any of the items are active from the outside
const activeItem = items?.find(item => item.active);
setActiveItemName(activeItem ? activeItem.name : null);
}, [items]);
const activeItemOptions = items?.find(item => item.name === activeItemName)?.options;
return (
<PanelSection title={title}>
<div className="flex flex-col bg-black">
<div className="bg-primary-dark mt-0.5 flex flex-wrap py-2">
{items?.map(item => {
return (
<Tooltip
position="bottom"
content={<span className="text-white">{item.name}</span>}
key={item.name}
>
<div
className="ml-2 mb-2"
onClick={() => {
if (item.disabled) {
return;
}
setActiveItemName(item.name);
item.onClick(item.name);
}}
>
<div
className={classnames(
'text-primary-active grid h-[40px] w-[40px] place-items-center rounded-md bg-black ',
activeItemName === item.name && 'bg-primary-light text-black',
item.disabled && 'opacity-50',
!item.disabled &&
'hover:bg-primary-light cursor-pointer hover:cursor-pointer hover:text-black'
)}
>
<Icon name={item.icon} />
</div>
</div>
</Tooltip>
);
})}
</div>
<div className="bg-primary-dark h-auto px-2">
<ToolSettings options={activeItemOptions} />
</div>
</div>
</PanelSection>
);
};
AdvancedToolbox.propTypes = {};
export default AdvancedToolbox;
@@ -0,0 +1,84 @@
import React from 'react';
import { ButtonGroup, InputRange, ButtonEnums } from '../../components';
const SETTING_TYPES = {
RANGE: 'range',
RADIO: 'radio',
CUSTOM: 'custom',
};
function ToolSettings({ options }) {
if (!options) {
return null;
}
const getButtons = option => {
const buttons = [];
option.values?.map(({ label, value: optionValue }, index) => {
buttons.push({
children: label,
onClick: () => option.onChange(optionValue),
key: `button-${option.id}-${index}`, // A unique key
});
});
return buttons;
};
return (
<div className="space-y-2 py-2 text-white">
{options?.map(option => {
if (option.type === SETTING_TYPES.RANGE) {
return (
<div
className="flex items-center"
key={option.id}
>
<div className="w-1/3 text-xs text-[13px]">{option.name}</div>
<div className="w-2/3">
<InputRange
minValue={option.min}
maxValue={option.max}
step={option.step}
value={option.value}
onChange={e => option.onChange(e)}
allowNumberEdit={true}
showAdjustmentArrows={false}
inputClassName="ml-2 w-4/5"
/>
</div>
</div>
);
}
if (option.type === SETTING_TYPES.RADIO) {
return (
<div
className="flex items-center justify-between text-[13px]"
key={option.id}
>
<span>{option.name}</span>
<div className="max-w-1/2">
<ButtonGroup
buttons={getButtons(option)}
defaultActiveIndex={option.defaultActiveIndex}
size={ButtonEnums.size.small}
/>
</div>
</div>
);
}
if (option.type === SETTING_TYPES.CUSTOM) {
return (
<div key={option.id}>
{typeof option.children === 'function' ? option.children() : option.children}
</div>
);
}
})}
</div>
);
}
export default ToolSettings;
@@ -0,0 +1,144 @@
import AdvancedToolbox from '../../AdvancedToolbox';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import {
createComponentTemplate,
createStoryMetaSettings,
} from '../../../storybook/functions/create-component-story';
export const argTypes = {
component: AdvancedToolbox,
title: 'Components/AdvancedToolbox',
};
<Meta
title="Components/AdvancedToolbox"
component={AdvancedToolbox}
/>
export const advancedToolboxTemplate = args => (
<div className="bg-primary-dark h-96 w-64">
<AdvancedToolbox {...args} />
</div>
);
<Heading
title="Advanced Toolbox"
componentRelativePath="AdvancedToolbox/AdvancedToolbox.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Usage](#usage)
- [Contribute](#contribute)
## Overview
OHIF advanced toolbox which can host set of tools that require more space for customization.
<Canvas>
<Story
name="Overview"
args={{
title: 'Segmentation Tools',
items: [
{
name: 'Brush',
icon: 'icon-tool-brush',
active: false,
onClick: () => console.log('Brush clicked'),
options: [
{
name: 'Radius (mm)',
type: 'range',
min: 1,
max: 10,
value: 5,
step: 1,
onChange: value => console.log('Brush size changed', value),
},
{
name: 'Mode',
type: 'radio',
value: 'Circle',
values: [
{ value: 'Circle', label: 'Circle' },
{ value: 'Sphere', label: 'Sphere' },
{ value: 'Rectangle', label: 'Rectangle' },
],
onChange: value => console.log('Brush mode changed', value),
},
],
},
{
name: 'Eraser',
icon: 'icon-tool-eraser',
onClick: () => console.log('eraser clicked'),
options: [
{
name: 'Mode',
type: 'radio',
value: 'EraserSphere',
values: [
{ value: 'EraserCircle', label: 'Circle' },
{ value: 'EraserSphere', label: 'Sphere' },
],
onChange: value => console.log('Brush mode changed', value),
},
],
},
{
name: 'Threshold',
icon: 'icon-tool-threshold',
active: true,
onClick: () => console.log('eraser clicked'),
options: [
{
name: 'Radius (mm)',
type: 'range',
min: 1,
max: 10,
value: 5,
step: 1,
onChange: value => console.log('Brush size changed', value),
},
{
name: 'Mode',
type: 'radio',
value: 'Circle',
values: [
{ value: 'Circle', label: 'Circle' },
{ value: 'Sphere', label: 'Sphere' },
{ value: 'Rectangle', label: 'Rectangle' },
],
onChange: value => console.log('Brush mode changed', value),
},
{
name: 'custom',
type: 'custom',
children: () => {
return (
<div>
<div>Custom</div>
<input type="text" />
</div>
);
},
},
],
},
],
}}
>
{advancedToolboxTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={AdvancedToolbox} />
## Usage
## Contribute
<Footer componentRelativePath="AdvancedToolbox/__stories__/AdvancedToolbox.stories.mdx" />
@@ -0,0 +1,2 @@
import AdvancedToolbox from './AdvancedToolbox';
export default AdvancedToolbox;
@@ -7,4 +7,9 @@ enum size {
small = 'small',
}
export { type, size };
enum orientation {
horizontal = 'horizontal',
vertical = 'vertical',
}
export { type, size, orientation };
@@ -66,7 +66,7 @@ There are different sizes for the button: `small`, and `medium`. The size refers
height.
<Canvas>
<Story name="Colors">
<Story name="Sizes">
<div className="flex items-center space-x-2">
<Button size={ButtonEnums.size.small}>Small Button</Button>
<Button size={ButtonEnums.size.medium}>Medium Button</Button>
@@ -1,150 +1,63 @@
import React, { useRef } from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
const baseButtonClass = 'border outline-none';
const roundedClasses = {
vertical: {
none: '',
small: 'first:rounded-t last:rounded-b',
medium: 'first:rounded-t-md last:rounded-b-md',
large: 'first:rounded-t-lg last:rounded-b-lg',
full: 'first:rounded-t-full last:rounded-b-full',
},
horizontal: {
none: '',
small: 'first:rounded-l last:rounded-r',
medium: 'first:rounded-l-md last:rounded-r-md',
large: 'first:rounded-l-lg last:rounded-r-lg',
full: 'first:rounded-l-full last:rounded-r-full',
},
};
const borderClasses = {
text: {
vertical: 'border-t-0 border-l-0 border-r-0 last:border-b-0',
horizontal: 'border-l-0 border-t-0 border-b-0 last:border-r-0',
},
outlined: {
vertical: 'border border-b-0 last:border-b',
horizontal: 'border border-r-0 last:border-r',
},
contained: {
vertical: 'border-t-0 border-l-0 border-r-0 last:border-b-0',
horizontal: 'border-l-0 border-t-0 border-b-0 last:border-r-0',
},
};
const variantClasses = {
text: {
default: 'border-primary-light',
primary: 'border-primary-main',
secondary: 'border-secondary-light',
white: 'border-white',
black: 'border-primary-main',
},
outlined: {
default: '',
primary: '',
secondary: 'border border-secondary-light',
white: '',
black: 'border-primary-main group-hover:bg-primary-main group-hover:border-black',
},
contained: {
default: 'border-white',
primary: 'border-white',
secondary: 'border-white',
white: 'border-black',
black: 'border-primary-main',
},
};
const orientationClasses = {
vertical: 'flex-col',
horizontal: 'flex-row',
};
const baseDisplayClass = 'inline-flex';
const fullWidthDisplayClass = 'flex';
// css class that are applied for buttons that not the first or last
const nonFirstLastClasses = {
vertical: 'border-t-0 border-b-0',
horizontal: 'border-l-0 last:border-r-0',
};
import { ButtonEnums } from '../../components';
const ButtonGroup = ({
children,
buttons,
onActiveIndexChange,
className,
disabled = false,
fullWidth = false,
color = 'default',
orientation = 'horizontal',
rounded = 'medium',
size = 'medium',
variant = 'outlined',
splitBorder = true,
...other
orientation = ButtonEnums.orientation.horizontal,
defaultActiveIndex = 0,
}) => {
const ref = useRef(null);
const [activeIndex, setActiveIndex] = useState(defaultActiveIndex);
const buttonClasses = classnames(
baseButtonClass,
borderClasses[variant] && borderClasses[variant][orientation],
variantClasses[variant] && variantClasses[variant][color],
roundedClasses[orientation] && roundedClasses[orientation][rounded]
);
const handleButtonClick = (e, index) => {
setActiveIndex(index);
onActiveIndexChange && onActiveIndexChange(index);
buttons[index].onClick && buttons[index].onClick(e);
};
const orientationClasses = {
horizontal: 'flex-row',
vertical: 'flex-col',
};
const wrapperClasses = classnames('inline-flex', orientationClasses[orientation], className);
return (
<div
role="group"
className={classnames(
'group',
className,
orientationClasses[orientation],
fullWidth ? fullWidthDisplayClass : baseDisplayClass
wrapperClasses,
'border-secondary-light rounded-[5px] border bg-black text-[13px] '
)}
ref={ref}
{...other}
>
{React.Children.map(children, (child, index) => {
const isFirst = index === 0;
const isLast = index === React.Children.count(children) - 1;
const isNotFirstOrLast = !isFirst && !isLast;
if (!React.isValidElement(child)) {
return null;
}
return React.cloneElement(child, {
className: classnames(
buttonClasses,
child.props.className,
!splitBorder && isNotFirstOrLast && nonFirstLastClasses[orientation],
!splitBorder && isLast && 'last:border-l-0'
),
disabled: child.props.disabled || disabled,
color: child.props.color || color,
fullWidth,
rounded: 'none',
size: child.props.size || size,
variant: child.props.variant || variant,
});
{buttons.map((buttonProps, index) => {
const isActive = index === activeIndex;
return (
<button
{...buttonProps}
key={index}
className={classnames(
'rounded-[4px] px-2 py-1',
isActive ? 'bg-customblue-40 text-white' : 'text-primary-active bg-black'
)}
onClick={e => handleButtonClick(e, index)}
/>
);
})}
</div>
);
};
ButtonGroup.propTypes = {
children: PropTypes.node.isRequired,
buttons: PropTypes.arrayOf(PropTypes.object).isRequired,
orientation: PropTypes.oneOf(Object.values(ButtonEnums.orientation)),
type: PropTypes.oneOf(Object.values(ButtonEnums.type)),
size: PropTypes.oneOf(Object.values(ButtonEnums.size)),
defaultActiveIndex: PropTypes.number,
onActiveIndexChange: PropTypes.func,
className: PropTypes.string,
color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary', 'white', 'black']),
disabled: PropTypes.bool,
fullWidth: PropTypes.bool,
orientation: PropTypes.oneOf(['vertical', 'horizontal']),
rounded: PropTypes.oneOf(['none', 'small', 'medium', 'large', 'full']),
size: PropTypes.oneOf(['small', 'medium', 'large', 'inherit']),
variant: PropTypes.oneOf(['text', 'outlined', 'contained']),
};
export default ButtonGroup;
@@ -1,5 +1,5 @@
import ButtonGroup from '../ButtonGroup';
import Button from '../../Button';
import { Button, ButtonGroup, ButtonEnums } from '../../../components';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import {
createComponentTemplate,
@@ -16,13 +16,9 @@ export const argTypes = {
component={ButtonGroup}
/>
export const buttonGroupTemplate = args => (
<ButtonGroup {...args}>
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
);
export const buttonGroupTemplate = args => {
return <ButtonGroup {...args} />;
};
<Heading
title="ButtonGroup"
@@ -39,7 +35,32 @@ export const buttonGroupTemplate = args => (
ButtonGroup is a container for a group of buttons.
<Canvas>
<Story name="Overview">{buttonGroupTemplate.bind({})}</Story>
<Story
name="Overview"
args={{
type: ButtonEnums.type.primary,
size: ButtonEnums.size.medium,
orientation: ButtonEnums.orientation.horizontal,
buttons: [
{
children: 'Button 1',
onClick: () => console.log('Button 1 clicked'),
},
{
children: 'Button 2',
onClick: () => console.log('Button 2 clicked'),
},
{
children: 'Button 3',
onClick: () => console.log('Button 3 clicked'),
},
],
activeIndex: 0,
onActiveIndexChange: index => console.log(index),
}}
>
{buttonGroupTemplate.bind({})}
</Story>
</Canvas>
## Props
@@ -48,56 +69,6 @@ ButtonGroup is a container for a group of buttons.
## Usage
### Variants
There are three different variants of ButtonGroup:
<Canvas>
<Story name="Variants">
<div className="flex flex-col space-y-2">
<ButtonGroup variant="text">
<Button>Text</Button>
<Button>Text</Button>
<Button>Text</Button>
</ButtonGroup>
<ButtonGroup variant="outlined">
<Button>Outlined</Button>
<Button>Outlined</Button>
<Button>Outlined</Button>
</ButtonGroup>
<ButtonGroup variant="contained">
<Button>Contained</Button>
<Button>Contained</Button>
<Button>Contained</Button>
</ButtonGroup>
</div>
</Story>
</Canvas>
### Orientation
You can change the orientation of the buttons in the group by setting the `orientation` prop.
<Canvas>
<Story name="Orientation Horizontal">
<ButtonGroup orientation="horizontal">
<Button>Horizontal-1</Button>
<Button>Horizontal-2</Button>
</ButtonGroup>
</Story>
</Canvas>
Or vertical
<Canvas>
<Story name="Orientation Vertical">
<ButtonGroup orientation="vertical">
<Button>Vertical-1</Button>
<Button>Vertical-2</Button>
</ButtonGroup>
</Story>
</Canvas>
## Contribute
<Footer componentRelativePath="ButtonGroup/__stories__/buttonGroup.stories.mdx" />
@@ -5,44 +5,88 @@ import classnames from 'classnames';
import Icon from '../Icon';
import Typography from '../Typography';
const Dropdown = ({ id, children, showDropdownIcon, list, titleClassName }) => {
const borderStyle = 'border-b last:border-b-0 border-secondary-main';
const Dropdown = ({
id,
children,
showDropdownIcon,
list,
itemsClassName,
titleClassName,
showBorders,
alignment,
// By default the max characters per line is the longest title
// if you wish to override this, you can pass in a number
maxCharactersPerLine,
}) => {
const [open, setOpen] = useState(false);
const element = useRef(null);
const DropdownItem = useCallback(({ id, title, icon, onClick }) => {
return (
<div
key={title}
className={classnames(
'hover:bg-secondary-main border-secondary-main flex cursor-pointer items-center border-b px-4 py-2 transition duration-300 last:border-b-0'
)}
onClick={() => {
setOpen(false);
onClick();
}}
data-cy={id}
>
{!!icon && (
<Icon
name={icon}
className="mr-2 w-4 text-white"
/>
)}
<Typography className={titleClassName}>{title}</Typography>
</div>
);
}, []);
// choose the max characters per line based on the longest title
const longestTitle = list.reduce((acc, item) => {
if (item.title.length > acc) {
return item.title.length;
}
return acc;
}, 0);
DropdownItem.defaultProps = {
icon: '',
};
maxCharactersPerLine = maxCharactersPerLine ?? longestTitle;
DropdownItem.propTypes = {
id: PropTypes.string,
title: PropTypes.string.isRequired,
icon: PropTypes.string,
onClick: PropTypes.func.isRequired,
};
const DropdownItem = useCallback(
({ id, title, icon, onClick }) => {
// Split the title into lines of length maxCharactersPerLine
const lines = [];
for (let i = 0; i < title.length; i += maxCharactersPerLine) {
lines.push(title.substring(i, i + maxCharactersPerLine));
}
return (
<div
key={title}
className={classnames(
'hover:bg-secondary-main flex cursor-pointer items-center px-4 py-2 transition duration-300 ',
titleClassName,
showBorders && borderStyle
)}
onClick={() => {
setOpen(false);
onClick();
}}
data-cy={id}
>
{!!icon && (
<Icon
name={icon}
className="mr-2 w-4 text-white"
/>
)}
<div
style={{
whiteSpace: 'nowrap',
}}
>
{title.length > maxCharactersPerLine && (
<div>
{lines.map((line, index) => (
<Typography
key={index}
className={itemsClassName}
>
{line}
</Typography>
))}
</div>
)}
{title.length <= maxCharactersPerLine && (
<Typography className={itemsClassName}>{title}</Typography>
)}
</div>
</div>
);
},
[maxCharactersPerLine, itemsClassName, titleClassName, showBorders]
);
const renderTitleElement = () => {
return (
@@ -72,8 +116,10 @@ const Dropdown = ({ id, children, showDropdownIcon, list, titleClassName }) => {
return (
<div
className={classnames(
'top-100 bg-primary-dark border-secondary-main absolute right-0 z-10 mt-2 origin-top-right transform rounded border shadow transition duration-300',
'top-100 border-secondary-main absolute z-10 mt-2 transform rounded border bg-black shadow transition duration-300',
{
'right-0 origin-top-right': alignment === 'right',
'left-0 origin-top-left': alignment === 'left',
'scale-0': !open,
'scale-100': open,
}
@@ -118,9 +164,10 @@ const Dropdown = ({ id, children, showDropdownIcon, list, titleClassName }) => {
</div>
);
};
Dropdown.defaultProps = {
showDropdownIcon: true,
maxCharactersPerLine: 20,
showBorders: true,
};
Dropdown.propTypes = {
@@ -136,6 +183,9 @@ Dropdown.propTypes = {
onClick: PropTypes.func.isRequired,
})
).isRequired,
alignment: PropTypes.oneOf(['left', 'right']),
maxCharactersPerLine: PropTypes.number,
showBorders: PropTypes.bool,
};
export default Dropdown;
+32 -14
View File
@@ -73,6 +73,8 @@ import tabROIThreshold from './../../assets/icons/tab-roi-threshold.svg';
import tabSegmentation from './../../assets/icons/tab-segmentation.svg';
import uiArrowDown from './../../assets/icons/ui-arrow-down.svg';
import uiArrowUp from './../../assets/icons/ui-arrow-up.svg';
import uiArrowLeft from './../../assets/icons/ui-arrow-left.svg';
import uiArrowRight from './../../assets/icons/ui-arrow-right.svg';
import loadingOHIFMark from './../../assets/icons/loading-ohif-mark.svg';
import notificationsInfo from './../../assets/icons/notifications-info.svg';
import notificationsWarning from './../../assets/icons/notifications-warning.svg';
@@ -115,16 +117,24 @@ import panelGroupOpenClose from './../../assets/icons/panel-group-open-close.svg
import rowAdd from './../../assets/icons/row-add.svg';
import rowEdit from './../../assets/icons/row-edit.svg';
import rowHidden from './../../assets/icons/row-hidden.svg';
import rowHideAll from './../../assets/icons/row-hide-all.svg';
import rowHide from './../../assets/icons/row-hide.svg';
import rowLocked from './../../assets/icons/row-locked.svg';
import rowShowAll from './../../assets/icons/row-show-all.svg';
import rowUnhide from './../../assets/icons/row-unhide.svg';
import rowUnlocked from './../../assets/icons/row-unlocked.svg';
import rowShown from './../../assets/icons/row-shown.svg';
import rowLock from './../../assets/icons/row-lock.svg';
import rowUnlock from './../../assets/icons/row-unlock.svg';
import iconMPR from './../../assets/icons/icon-mpr-alt.svg';
import checkboxDefault from './../../assets/icons/checkbox-default.svg';
import checkboxActive from './../../assets/icons/checkbox-active.svg';
import referenceLines from './../../assets/icons/reference-lines.svg';
import chevronDownNew from './../../assets/icons/icon-disclosure-close.svg';
import chevronLeftNew from './../../assets/icons/icon-disclosure-open.svg';
import settingsBars from './../../assets/icons/icon-display-settings.svg';
import iconAdd from './../../assets/icons/icon-add.svg';
import iconRename from './../../assets/icons/icon-rename.svg';
import iconDelete from './../../assets/icons/icon-delete.svg';
import iconMoreMenu from './../../assets/icons/icon-more-menu.svg';
import iconToolBrush from './../../assets/icons/icon-tool-brush.svg';
import iconToolEraser from './../../assets/icons/icon-tool-eraser.svg';
import iconToolScissor from './../../assets/icons/icon-tool-scissor.svg';
import iconToolThreshold from './../../assets/icons/icon-tool-threshold.svg';
/** Old OHIF */
import oldTrash from './../../assets/icons/old-trash.svg';
@@ -164,7 +174,6 @@ const ICONS = {
'icon-transferring': iconTransferring,
'info-action': infoAction,
'info-link': infoLink,
'arrow-left': arrowLeft,
'launch-arrow': launchArrow,
'launch-info': launchInfo,
link: link,
@@ -195,12 +204,9 @@ const ICONS = {
'row-add': rowAdd,
'row-edit': rowEdit,
'row-hidden': rowHidden,
'row-hide-all': rowHideAll,
'row-hide': rowHide,
'row-locked': rowLocked,
'row-show-all': rowShowAll,
'row-unhide': rowUnhide,
'row-unlocked': rowUnlocked,
'row-shown': rowShown,
'row-lock': rowLock,
'row-unlock': rowUnlock,
'checkbox-checked': checkboxChecked,
'checkbox-unchecked': checkboxUnchecked,
'loading-ohif-mark': loadingOHIFMark,
@@ -237,7 +243,6 @@ const ICONS = {
'tool-create-threshold': toolCreateThreshold,
'tool-calibration': toolCalibration,
'tool-point': toolCircle,
'tool-circle': toolCircle,
'tool-freehand-line': toolFreehand,
'tool-freehand-polygon': toolFreehandPolygon,
'tool-polygon': toolPolygon,
@@ -256,9 +261,22 @@ const ICONS = {
'tab-segmentation': tabSegmentation,
'ui-arrow-down': uiArrowDown,
'ui-arrow-up': uiArrowUp,
'ui-arrow-left': uiArrowLeft,
'ui-arrow-right': uiArrowRight,
'checkbox-default': checkboxDefault,
'checkbox-active': checkboxActive,
'tool-referenceLines': referenceLines,
'chevron-left-new': chevronLeftNew,
'chevron-down-new': chevronDownNew,
'settings-bars': settingsBars,
'icon-rename': iconRename,
'icon-add': iconAdd,
'icon-delete': iconDelete,
'icon-more-menu': iconMoreMenu,
'icon-tool-brush': iconToolBrush,
'icon-tool-eraser': iconToolEraser,
'icon-tool-scissor': iconToolScissor,
'icon-tool-threshold': iconToolThreshold,
/** Old OHIF */
'old-trash': oldTrash,
@@ -0,0 +1,4 @@
.input-range-thumb-design {
@apply bg-primary-light border-primary-dark border-[2px] border-solid;
border-radius: 50%;
}
@@ -0,0 +1,218 @@
import React, { useState, useCallback, useEffect, useRef } from 'react';
import classNames from 'classnames';
import { InputNumber } from '../../components'; // Import InputNumber component
import './InputDoubleRange.css';
type InputDoubleRangeProps = {
values: [number, number];
onChange: (values: [number, number]) => void;
minValue?: number;
maxValue?: number;
step?: number;
unit?: string;
containerClassName?: string;
inputClassName?: string;
labelClassName?: string;
labelVariant?: string;
showLabel?: boolean;
labelPosition?: 'left' | 'right';
trackColor?: string;
allowNumberEdit?: boolean;
showAdjustmentArrows?: boolean;
};
const InputDoubleRange: React.FC<InputDoubleRangeProps> = ({
values,
onChange,
minValue,
maxValue,
step,
unit,
containerClassName,
inputClassName,
labelClassName,
labelVariant,
showLabel,
labelPosition,
trackColor,
allowNumberEdit,
showAdjustmentArrows,
}) => {
// Set initial thumb positions as percentages
const initialPercentageStart = Math.round(((values[0] - minValue) / (maxValue - minValue)) * 100);
const initialPercentageEnd = Math.round(((values[1] - minValue) / (maxValue - minValue)) * 100);
const [percentageStart, setPercentageStart] = useState(initialPercentageStart);
const [percentageEnd, setPercentageEnd] = useState(initialPercentageEnd);
const [rangeValue, setRangeValue] = useState(values);
const selectedThumbRef = useRef(null);
const sliderRef = useRef(null);
const updateRangeValues = (newValues, index = null) => {
const updatedRangeValue = Array.isArray(newValues) ? [...newValues] : [...rangeValue];
if (index !== null) {
updatedRangeValue[index] = newValues;
}
const calculatePercentage = value => ((value - minValue) / (maxValue - minValue)) * 100;
const newPercentageStart = calculatePercentage(updatedRangeValue[0]);
const newPercentageEnd = calculatePercentage(updatedRangeValue[1]);
setRangeValue(updatedRangeValue);
onChange(updatedRangeValue);
setPercentageStart(newPercentageStart);
setPercentageEnd(newPercentageEnd);
};
useEffect(() => {
updateRangeValues(values);
}, [values, minValue, maxValue]);
const LabelOrEditableNumber = (val, index) => {
return allowNumberEdit ? (
<InputNumber
minValue={minValue}
maxValue={maxValue}
value={val}
onChange={newValue => {
updateRangeValues(newValue, index);
}}
step={step}
labelClassName="text-white"
showAdjustmentArrows={showAdjustmentArrows}
/>
) : (
<span className={classNames(labelClassName ?? 'text-white')}>
{val}
{unit}
</span>
);
};
useEffect(() => {
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleGlobalMouseUp);
};
}, []);
const handleGlobalMouseUp = () => {
// Remove global mouse event listeners
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleGlobalMouseUp);
selectedThumbRef.current = null;
};
const handleMouseDown = e => {
const rect = sliderRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const percentageClicked = (x / rect.width) * 100;
// Calculate the distances from the clicked point to both thumbs' positions
const distanceToStartThumb = Math.abs(percentageClicked - percentageStart);
const distanceToEndThumb = Math.abs(percentageClicked - percentageEnd);
// Check if the clicked point is within a threshold distance to either thumb
if (distanceToStartThumb < 10) {
selectedThumbRef.current = 0;
} else if (distanceToEndThumb < 10) {
selectedThumbRef.current = 1;
}
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleGlobalMouseUp);
};
const handleMouseMove = e => {
const selectedThumbValue = selectedThumbRef.current;
if (selectedThumbValue === null) {
return;
}
const rect = sliderRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const newValue =
Math.round(((x / rect.width) * (maxValue - minValue) + minValue) / step) * step;
// Make sure newValue is within [minValue, maxValue]
const clampedValue = Math.min(Math.max(newValue, minValue), maxValue);
// Ensure that left and right thumbs don't switch positions
if (selectedThumbValue === 0 && clampedValue >= rangeValue[1]) {
return;
}
if (selectedThumbValue === 1 && clampedValue <= rangeValue[0]) {
return;
}
// Update the correct values in the rangeValue array
const updatedRangeValue = [...rangeValue];
updatedRangeValue[selectedThumbValue] = clampedValue;
setRangeValue(updatedRangeValue);
onChange(updatedRangeValue);
// Update the thumb position
const percentage = Math.round(((clampedValue - minValue) / (maxValue - minValue)) * 100);
if (selectedThumbValue === 0) {
setPercentageStart(percentage);
} else {
setPercentageEnd(percentage);
}
};
// Calculate the range values percentages for gradient background
const rangeValuePercentageStart = ((rangeValue[0] - minValue) / (maxValue - minValue)) * 100;
const rangeValuePercentageEnd = ((rangeValue[1] - minValue) / (maxValue - minValue)) * 100;
return (
<div className={`flex select-none items-center space-x-2 ${containerClassName ?? ''}`}>
{showLabel && LabelOrEditableNumber(rangeValue[0], 0)}
<div
className="relative flex h-10 w-full items-center"
onMouseDown={handleMouseDown}
ref={sliderRef}
>
<div
className="h-[3px] w-full rounded-lg"
style={{
background: `linear-gradient(to right, #3a3f99 0%, #3a3f99 ${rangeValuePercentageStart}%, #5acce6 ${rangeValuePercentageStart}%, #5acce6 ${rangeValuePercentageEnd}%, #3a3f99 ${rangeValuePercentageEnd}%, #3a3f99 100%)`,
}}
></div>
<div
className="input-range-thumb-design absolute h-3 w-3 cursor-pointer"
style={{
left: `calc(${percentageStart}% - 3px)`,
}}
></div>
<div
className="input-range-thumb-design absolute h-3 w-3 cursor-pointer rounded-full"
style={{ left: `calc(${percentageEnd}% - 3px)` }}
></div>
</div>
{showLabel && LabelOrEditableNumber(rangeValue[1], 1)}
</div>
);
};
InputDoubleRange.defaultProps = {
minValue: 0,
maxValue: 100,
step: 1,
unit: '',
containerClassName: '',
inputClassName: '',
labelClassName: '',
labelVariant: 'body1',
showLabel: false,
labelPosition: 'left',
trackColor: 'primary',
allowNumberEdit: false,
showAdjustmentArrows: false,
};
export default InputDoubleRange;
@@ -0,0 +1,56 @@
import { InputDoubleRange } from '../../../components';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
component: InputDoubleRange,
title: 'Components/InputDoubleRange',
};
<Meta
title="Components/InputDoubleRange"
component={InputDoubleRange}
/>
export const RangeInputTemplate = args => (
<div className="w-12">
<InputDoubleRange {...args} />
</div>
);
<Heading
title="InputDoubleRange"
componentRelativePath="InputDoubleRange/InputDoubleRange.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
InputDoubleRange is a component that allows you to use as a boolean value
<Canvas>
<Story
name="Overview"
args={{
values: [100, 200],
minValue: 0,
maxValue: 300,
step: 10,
unit: '%',
onChange: () => console.log('input range change'),
labelClassName: 'text-black',
}}
>
{RangeInputTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={InputDoubleRange} />
## Contribute
<Footer componentRelativePath="InputDoubleRange/__stories__/InputDoubleRange.stories.mdx" />
@@ -0,0 +1,3 @@
import InputDoubleRange from './InputDoubleRange';
export default InputDoubleRange;
@@ -2,6 +2,18 @@ input[type='text'] {
-webkit-appearance: none;
}
/* For most modern browsers */
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* For Firefox */
input[type='number'] {
-moz-appearance: textfield;
}
.input-number:focus {
border: none;
outline: none;
@@ -1,8 +1,9 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import IconButton from '../IconButton';
import Icon from '../Icon';
import './InputNumber.css';
import Label from '../Label';
import getMaxDigits from '../../utils/getMaxDigits';
/**
* React Number Input component'
@@ -20,14 +21,15 @@ const sizesClasses = {
const InputNumber: React.FC<{
value: number;
onChange: (value) => void;
onChange: (value: number) => void;
minValue?: number;
maxValue?: number;
step: number;
size?: string;
step?: number;
size?: 'sm' | 'lg';
className?: string;
labelClassName?: string;
label?: string;
showAdjustmentArrows?: boolean;
}> = ({
value,
onChange,
@@ -38,96 +40,99 @@ const InputNumber: React.FC<{
maxValue = 100,
labelClassName,
label,
showAdjustmentArrows = true,
}) => {
const [numberValue, setNumberValue] = useState(value);
const maxDigits = getMaxDigits(maxValue, step);
const inputWidth = Math.max(maxDigits * 10, showAdjustmentArrows ? 20 : 28);
const arrowWidth = showAdjustmentArrows ? 20 : 0;
const containerWidth = `${inputWidth + arrowWidth}px`;
useEffect(() => {
setNumberValue(value);
}, [value]);
const handleMinMax = useCallback(
(value: number) => {
if (value > maxValue) {
return maxValue;
} else if (value < minValue) {
return minValue;
} else {
return value;
}
},
(val: number) => Math.min(Math.max(val, minValue), maxValue),
[maxValue, minValue]
);
const handleChange = useCallback(
e => {
const numberValue = handleMinMax(Number(e.target.value));
setNumberValue(numberValue);
onChange(numberValue);
},
[onChange, setNumberValue, handleMinMax]
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
const handleIncrement = useCallback(
e => {
const newNum = handleMinMax(Number(numberValue) + step);
setNumberValue(newNum);
onChange(newNum);
},
[onChange, setNumberValue, step, numberValue, handleMinMax]
);
// Allow negative sign, empty string, or single decimal point for user flexibility
if (inputValue === '-' || inputValue === '' || inputValue === '.') {
setNumberValue(inputValue);
return;
}
const handleDecrement = useCallback(
e => {
const newNum = handleMinMax(Number(numberValue) - step);
setNumberValue(newNum);
onChange(newNum);
},
[onChange, setNumberValue, step, numberValue, handleMinMax]
);
const number = Number(inputValue);
// Filter out invalid inputs like 'NaN'
if (!isNaN(number)) {
updateValue(number);
}
};
const updateValue = (val: number) => {
const newValue = handleMinMax(val);
setNumberValue(newValue);
onChange(newValue);
};
const increment = () => updateValue(numberValue + step);
const decrement = () => updateValue(numberValue - step);
return (
<div className={'flex flex-1 flex-col'}>
<div className="flex flex-1 flex-col">
{label && (
<Label
className={labelClassName}
text={label}
></Label>
/>
)}
<div
className={`border-secondary-light flex items-center justify-center overflow-hidden rounded-md border-2 bg-black px-1 ${
sizesClasses[size]
} ${className ? className : ''}`}
} ${className || ''}`}
style={{ width: containerWidth }}
>
<div className="flex">
<input
type="text"
type="number"
value={numberValue}
step={step}
onChange={handleChange}
className={`input-number w-full bg-black text-center text-[12px] text-white`}
className={'input-number w-full bg-black text-center text-[12px] text-white'}
style={{ width: inputWidth }}
/>
<div className="up-arrowsize flex flex-col items-center justify-around">
<IconButton
id={'down-arrow-icon'}
variant="text"
color="inherit"
size="initial"
className="rotate-180 transform text-[#726f7e]"
onClick={handleIncrement}
>
<Icon name="ui-arrow-down" />
</IconButton>
<IconButton
id={'down-arrow-icon'}
variant="text"
color="inherit"
size="initial"
className="text-[#726f7e]"
onClick={handleDecrement}
>
<Icon name="ui-arrow-down" />
</IconButton>
</div>
{showAdjustmentArrows && (
<div className="up-arrowsize flex flex-col items-center justify-around">
<ArrowButton
onClick={increment}
rotate
/>
<ArrowButton onClick={decrement} />
</div>
)}
</div>
</div>
</div>
);
};
const ArrowButton = ({ onClick, rotate = false }: { onClick: () => void; rotate?: boolean }) => (
<IconButton
id="arrow-icon"
variant="text"
color="inherit"
size="initial"
className={`text-[#726f7e] ${rotate ? 'rotate-180 transform' : ''}`}
onClick={onClick}
>
<Icon name="ui-arrow-down" />
</IconButton>
);
export default InputNumber;
@@ -35,8 +35,11 @@ InputNumber is a component that allows you to use as a number value
name="Overview"
args={{
step: 1,
value: 3,
value: 1000,
minValue: 0,
maxValue: 1000,
size: 'sm',
showAdjustmentArrows: true,
onChange: () => console.log('input range change'),
}}
>
@@ -4,23 +4,30 @@ input[type='range'] {
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
border: none;
@apply border-primary-dark bg-primary-light border-[2px] border-solid;
height: 13px;
width: 13px;
border-radius: 50%;
background: #5acce6;
}
input[type='range']::-moz-range-thumb {
-webkit-appearance: none;
border: none;
@apply border-primary-dark bg-primary-light border-[2px] border-solid;
height: 13px;
width: 13px;
border-radius: 50%;
background: #5acce6;
}
.input-range-thumb-design[type='range']::-webkit-slider-thumb {
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.521);
border: 1.5px solid #090c29;
input[type='range']::-webkit-slider-runnable-track {
border: none !important;
}
input[type='range']::-moz-range-track {
border: none !important;
}
.range-track {
position: absolute;
top: 0;
left: 0;
right: 0;
}
@@ -1,7 +1,8 @@
import React, { useState, useCallback, useEffect } from 'react';
import classNames from 'classnames';
import Typography from '../Typography';
import { InputNumber } from '../../components';
import './InputRange.css';
import getMaxDigits from '../../utils/getMaxDigits';
/**
* React Range Input component
@@ -11,10 +12,9 @@ import './InputRange.css';
*
*
*/
const InputRange: React.FC<{
type InputRangeProps = {
value: number;
onChange: (value) => void;
onChange: (value: number) => void;
minValue: number;
maxValue: number;
step: number;
@@ -26,7 +26,11 @@ const InputRange: React.FC<{
showLabel?: boolean;
labelPosition?: string;
trackColor?: string;
}> = ({
allowNumberEdit?: boolean;
showAdjustmentArrows?: boolean;
};
const InputRange: React.FC<InputRangeProps> = ({
value,
onChange,
minValue,
@@ -38,72 +42,76 @@ const InputRange: React.FC<{
labelClassName,
labelVariant,
showLabel = true,
labelPosition = '',
labelPosition = 'right',
trackColor,
allowNumberEdit = false,
showAdjustmentArrows = true,
}) => {
const [rangeValue, setRangeValue] = useState(value);
// Allow for the value property to update the range value.
const maxDigits = getMaxDigits(maxValue, step);
const labelWidth = `${maxDigits * 10}px`;
useEffect(() => setRangeValue(value), [value]);
const handleChange = useCallback(
e => {
const rangeValue = Number(e.target.value);
setRangeValue(rangeValue);
onChange(rangeValue);
const val = Number(e.target.value);
setRangeValue(val);
onChange(val);
},
[onChange, setRangeValue]
[onChange]
);
const rangeValuePercentage = ((rangeValue - minValue) / (maxValue - minValue)) * 100;
const rangeValueForStr = step >= 1 ? rangeValue.toFixed(0) : rangeValue.toFixed(1);
const LabelOrEditableNumber = allowNumberEdit ? (
<InputNumber
minValue={minValue}
maxValue={maxValue}
value={rangeValue}
onChange={val => {
setRangeValue(val);
onChange(val);
}}
step={step}
showAdjustmentArrows={showAdjustmentArrows}
/>
) : (
<span className={classNames(labelClassName ?? 'text-white')}>
{rangeValue}
{unit}
</span>
);
return (
<div
className={`flex cursor-pointer items-center space-x-1 ${
containerClassName ? containerClassName : ''
}`}
className={`flex cursor-pointer items-center ${containerClassName ?? ''}`}
onClick={e => {
e.stopPropagation();
e.preventDefault();
}}
>
{showLabel && labelPosition === 'left' && (
<Typography
variant={labelVariant ?? 'subtitle'}
component="p"
className={classNames('w-8', labelClassName ?? 'text-white')}
>
{rangeValueForStr}
{unit}
</Typography>
)}
<input
type="range"
min={minValue}
max={maxValue}
value={rangeValue}
className={`input-range-thumb-design h-[3px] appearance-none rounded-lg ${
inputClassName ? inputClassName : ''
}`}
style={{
background:
trackColor ||
`linear-gradient(to right, #5acce6 0%, #5acce6 ${rangeValuePercentage - 10}%, #3a3f99 ${
rangeValuePercentage + 10
}%, #3a3f99 100%)`,
}}
onChange={handleChange}
id="myRange"
step={step}
/>
{showLabel && (!labelPosition || labelPosition === 'right') && (
<Typography
variant={labelVariant ?? 'subtitle'}
component="p"
className={classNames('w-8', labelClassName ?? 'text-white')}
>
{rangeValueForStr}
{unit}
</Typography>
)}
<div className="relative flex w-full items-center">
{showLabel && labelPosition === 'left' && LabelOrEditableNumber}
<div className="range-track"></div>
<input
type="range"
min={minValue}
max={maxValue}
value={rangeValue}
className={`h-[3px] appearance-none rounded-md ${inputClassName ?? ''}`}
style={{
background: `linear-gradient(to right, #5acce6 0%, #5acce6 ${rangeValuePercentage}%, #3a3f99 ${rangeValuePercentage}%, #3a3f99 100%)`,
}}
onChange={handleChange}
id="myRange"
step={step}
/>
{showLabel && labelPosition === 'right' && (
<div style={{ width: labelWidth }}>{LabelOrEditableNumber}</div>
)}
</div>
</div>
);
};
@@ -1,4 +1,4 @@
import InputRange from '../InputRange';
import { InputRange } from '../../../components';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
@@ -36,7 +36,7 @@ InputRange is a component that allows you to use as a boolean value
args={{
value: 10,
minValue: 0,
maxValue: 300,
maxValue: 100,
step: 10,
unit: '%',
onChange: () => console.log('input range change'),
@@ -0,0 +1,150 @@
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
const baseButtonClass = 'border outline-none';
const roundedClasses = {
vertical: {
none: '',
small: 'first:rounded-t last:rounded-b',
medium: 'first:rounded-t-md last:rounded-b-md',
large: 'first:rounded-t-lg last:rounded-b-lg',
full: 'first:rounded-t-full last:rounded-b-full',
},
horizontal: {
none: '',
small: 'first:rounded-l last:rounded-r',
medium: 'first:rounded-l-md last:rounded-r-md',
large: 'first:rounded-l-lg last:rounded-r-lg',
full: 'first:rounded-l-full last:rounded-r-full',
},
};
const borderClasses = {
text: {
vertical: 'border-t-0 border-l-0 border-r-0 last:border-b-0',
horizontal: 'border-l-0 border-t-0 border-b-0 last:border-r-0',
},
outlined: {
vertical: 'border border-b-0 last:border-b',
horizontal: 'border border-r-0 last:border-r',
},
contained: {
vertical: 'border-t-0 border-l-0 border-r-0 last:border-b-0',
horizontal: 'border-l-0 border-t-0 border-b-0 last:border-r-0',
},
};
const variantClasses = {
text: {
default: 'border-primary-light',
primary: 'border-primary-main',
secondary: 'border-secondary-light',
white: 'border-white',
black: 'border-primary-main',
},
outlined: {
default: '',
primary: '',
secondary: 'border border-secondary-light',
white: '',
black: 'border-primary-main group-hover:bg-primary-main group-hover:border-black',
},
contained: {
default: 'border-white ',
primary: 'border-white ',
secondary: 'border-white ',
white: 'border-black',
black: 'border-primary-main',
},
};
const orientationClasses = {
vertical: 'flex-col',
horizontal: 'flex-row',
};
const baseDisplayClass = 'inline-flex';
const fullWidthDisplayClass = 'flex';
// css class that are applied for buttons that not the first or last
const nonFirstLastClasses = {
vertical: 'border-t-0 border-b-0',
horizontal: 'border-l-0 last:border-r-0',
};
const LegacyButtonGroup = ({
children,
className,
disabled = false,
fullWidth = false,
color = 'default',
orientation = 'horizontal',
rounded = 'medium',
size = 'medium',
variant = 'outlined',
splitBorder = true,
...other
}) => {
const ref = useRef(null);
const buttonClasses = classnames(
baseButtonClass,
borderClasses[variant] && borderClasses[variant][orientation],
variantClasses[variant] && variantClasses[variant][color],
roundedClasses[orientation] && roundedClasses[orientation][rounded]
);
return (
<div
role="group"
className={classnames(
'group',
className,
orientationClasses[orientation],
fullWidth ? fullWidthDisplayClass : baseDisplayClass
)}
ref={ref}
{...other}
>
{React.Children.map(children, (child, index) => {
const isFirst = index === 0;
const isLast = index === React.Children.count(children) - 1;
const isNotFirstOrLast = !isFirst && !isLast;
if (!React.isValidElement(child)) {
return null;
}
return React.cloneElement(child, {
className: classnames(
buttonClasses,
child.props.className,
!splitBorder && isNotFirstOrLast && nonFirstLastClasses[orientation],
!splitBorder && isLast && 'last:border-l-0'
),
disabled: child.props.disabled || disabled,
color: child.props.color || color,
fullWidth,
rounded: 'none',
size: child.props.size || size,
variant: child.props.variant || variant,
});
})}
</div>
);
};
LegacyButtonGroup.propTypes = {
children: PropTypes.node.isRequired,
className: PropTypes.string,
color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary', 'white', 'black']),
disabled: PropTypes.bool,
fullWidth: PropTypes.bool,
orientation: PropTypes.oneOf(['vertical', 'horizontal']),
rounded: PropTypes.oneOf(['none', 'small', 'medium', 'large', 'full']),
size: PropTypes.oneOf(['small', 'medium', 'large', 'inherit']),
variant: PropTypes.oneOf(['text', 'outlined', 'contained']),
};
export default LegacyButtonGroup;
@@ -0,0 +1,102 @@
import { LegacyButtonGroup, LegacyButton } from '../../../components';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import {
createComponentTemplate,
createStoryMetaSettings,
} from '../../../storybook/functions/create-component-story';
export const argTypes = {
component: LegacyButtonGroup,
title: 'Components/LegacyButtonGroup',
};
<Meta
title="Components/LegacyButtonGroup"
component={LegacyButtonGroup}
/>
export const LegacybuttonGroupTemplate = args => (
<LegacyButtonGroup {...args}>
<LegacyButton>One</LegacyButton>
<LegacyButton>Two</LegacyButton>
<LegacyButton>Three</LegacyButton>
</LegacyButtonGroup>
);
<Heading
title="LegacyButtonGroup"
componentRelativePath="LegacyButtonGroup/LegacyButtonGroup.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Usage](#usage)
- [Contribute](#contribute)
## Overview
LegacyButtonGroup is a container for a group of buttons.
<Canvas>
<Story name="Overview">{LegacybuttonGroupTemplate.bind({})}</Story>
</Canvas>
## Props
<ArgsTable of={LegacyButtonGroup} />
## Usage
### Variants
There are three different variants of LegacyButtonGroup:
<Canvas>
<Story name="Variants">
<div className="flex flex-col space-y-2">
<LegacyButtonGroup variant="text">
<LegacyButton>Text</LegacyButton>
<LegacyButton>Text</LegacyButton>
<LegacyButton>Text</LegacyButton>
</LegacyButtonGroup>
<LegacyButtonGroup variant="outlined">
<LegacyButton>Outlined</LegacyButton>
<LegacyButton>Outlined</LegacyButton>
<LegacyButton>Outlined</LegacyButton>
</LegacyButtonGroup>
<LegacyButtonGroup variant="contained">
<LegacyButton>Contained</LegacyButton>
<LegacyButton>Contained</LegacyButton>
<LegacyButton>Contained</LegacyButton>
</LegacyButtonGroup>
</div>
</Story>
</Canvas>
### Orientation
You can change the orientation of the buttons in the group by setting the `orientation` prop.
<Canvas>
<Story name="Orientation Horizontal">
<LegacyButtonGroup orientation="horizontal">
<LegacyButton>Horizontal-1</LegacyButton>
<LegacyButton>Horizontal-2</LegacyButton>
</LegacyButtonGroup>
</Story>
</Canvas>
Or vertical
<Canvas>
<Story name="Orientation Vertical">
<LegacyButtonGroup orientation="vertical">
<LegacyButton>Vertical-1</LegacyButton>
<LegacyButton>Vertical-2</LegacyButton>
</LegacyButtonGroup>
</Story>
</Canvas>
## Contribute
<Footer componentRelativePath="LEgacyButtonGroup/__stories__/legacyButtonGroup.stories.mdx" />
@@ -0,0 +1,2 @@
import LegacyButtonGroup from './LegacyButtonGroup';
export default LegacyButtonGroup;
@@ -5,7 +5,7 @@ import { StringNumber } from '../../types';
import LegacyPatientInfo from '../LegacyPatientInfo';
import Icon from '../Icon';
import ButtonGroup from '../ButtonGroup';
import LegacyButtonGroup from '../LegacyButtonGroup';
import LegacyButton from '../LegacyButton';
import LegacyCinePlayer from '../LegacyCinePlayer';
@@ -89,8 +89,8 @@ const LegacyViewportActionBar = ({
className="mt-2"
style={{ pointerEvents: 'all' }}
>
{/* TODO Revisit design of ButtonGroup later - for now use LegacyButton for its children.*/}
<ButtonGroup>
{/* TODO Revisit design of LegacyButtonGroup later - for now use LegacyButton for its children.*/}
<LegacyButtonGroup>
<LegacyButton
size="initial"
className="bg-black px-2 py-1"
@@ -113,7 +113,7 @@ const LegacyViewportActionBar = ({
className="w-4 text-white"
/>
</LegacyButton>
</ButtonGroup>
</LegacyButtonGroup>
</div>
)}
{showCine && !showNavArrows && (
@@ -0,0 +1,61 @@
import React, { useState } from 'react';
import { Icon } from '@ohif/ui';
import PropTypes from 'prop-types';
const PanelSection = ({ title, children, actionIcons = [] }) => {
const [areChildrenVisible, setChildrenVisible] = useState(true);
const handleHeaderClick = () => {
setChildrenVisible(!areChildrenVisible);
};
return (
<>
<div
className="bg-secondary-dark mt-[2px] flex h-7 cursor-pointer select-none items-center justify-between rounded-[4px] pl-2.5 text-[13px]"
onClick={handleHeaderClick}
>
<div className="text-aqua-pale">{title}</div>
<div className="flex items-center space-x-1">
{actionIcons.map((icon, index) => (
<Icon
key={index}
name={icon.name}
onClick={e => {
e.stopPropagation();
if (!areChildrenVisible) {
setChildrenVisible(true);
}
icon.onClick();
}}
/>
))}
<div className="grid h-[28px] w-[28px] place-items-center">
<Icon name={areChildrenVisible ? 'chevron-down-new' : 'chevron-left-new'} />
</div>
</div>
</div>
{areChildrenVisible && (
<>
<div className="h-[2px] bg-black"></div>
<div className="bg-primary-dark rounded-b-[4px]">{children}</div>
</>
)}
</>
);
};
PanelSection.defaultProps = {};
PanelSection.propTypes = {
title: PropTypes.string,
children: PropTypes.node,
actionIcons: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
onClick: PropTypes.func,
})
),
};
export default PanelSection;
@@ -0,0 +1,71 @@
import { PanelSection, SidePanel } from '../../../components';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
export const argTypes = {
component: PanelSection,
title: 'Components/PanelSection',
};
<Meta
title="Components/PanelSection"
component={PanelSection}
/>
export const PanelSectionTemplate = args => (
<div className="w-96">
<SidePanel
side="left"
defaultComponentOpen="Example"
tabs={[
{
id: 'tab1',
name: 'tab1',
label: 'Tab 1',
iconName: 'group-layers',
iconLabel: 'group-layers',
content: () => (
<PanelSection {...args}>
<div className="text-white">{'this is children'}</div>
</PanelSection>
),
},
]}
/>
</div>
);
<Heading
title="PanelSection"
componentRelativePath="PanelSection/PanelSection.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
PanelSection is a component that renders the PanelSections.
<Canvas>
<Story
name="Overview"
args={{
title: 'Segmentation Tools',
}}
>
{PanelSectionTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={PanelSection} />
## Usage
## Contribute
<Footer componentRelativePath="PanelSection/__stories__/PanelSection.stories.mdx" />
@@ -0,0 +1,2 @@
import PanelSection from './PanelSection';
export default PanelSection;
@@ -0,0 +1,23 @@
import React from 'react';
import Icon from '../Icon';
function AddSegmentRow({ onClick }) {
return (
<div
className="flex hover:cursor-pointer"
onClick={onClick}
>
<div className="h-[28px] w-[28px]"></div>
<div className="group ml-2.5 mt-1">
<div className="text-primary-active group-hover:bg-secondary-dark flex items-center rounded-[4px] pr-2">
<div className="grid h-[28px] w-[28px] place-items-center">
<Icon name="icon-add" />
</div>
<span className="text-[13px]">Add Segment</span>
</div>
</div>
</div>
);
}
export default AddSegmentRow;
@@ -0,0 +1,20 @@
import React from 'react';
import Icon from '../Icon';
function NoSegmentationRow({ onSegmentationAdd }) {
return (
<div
className="group"
onClick={onSegmentationAdd}
>
<div className="text-primary-active group-hover:bg-secondary-dark flex items-center rounded-[4px] group-hover:cursor-pointer">
<div className="grid h-[28px] w-[28px] place-items-center">
<Icon name="icon-add" />
</div>
<span className="text-[13px]">Add Segmentation</span>
</div>
</div>
);
}
export default NoSegmentationRow;
@@ -1,10 +1,13 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import Icon from '../Icon';
import InputRange from '../InputRange';
import CheckBox from '../CheckBox';
import InputNumber from '../InputNumber';
import classNames from 'classnames';
const getRoundedValue = value => {
return Math.round(value * 100) / 100;
};
const ActiveSegmentationConfig = ({
config,
@@ -15,7 +18,7 @@ const ActiveSegmentationConfig = ({
setFillAlpha,
}) => {
return (
<div className="flex justify-between px-2 pt-[13px] text-[12px]">
<div className="flex justify-between px-3 pt-[13px] text-[12px]">
<div className="flex flex-col items-start">
<div className="mb-[12px] text-white">Active</div>
<CheckBox
@@ -39,7 +42,7 @@ const ActiveSegmentationConfig = ({
<InputRange
minValue={0}
maxValue={100}
value={config.outlineOpacity * 100}
value={getRoundedValue(config.outlineOpacity * 100)}
onChange={setOutlineOpacityActive}
step={1}
containerClassName="mt-[4px] mb-[9px]"
@@ -48,9 +51,9 @@ const ActiveSegmentationConfig = ({
unit="%"
/>
<InputRange
minValue={0}
minValue={4}
maxValue={100}
value={config.fillAlpha * 100}
value={getRoundedValue(config.fillAlpha * 100)}
onChange={setFillAlpha}
step={1}
containerClassName="mt-[4px] mb-[9px]"
@@ -80,11 +83,11 @@ const InactiveSegmentationConfig = ({
setFillAlphaInactive,
}) => {
return (
<div className="px-2">
<div className="px-3">
<CheckBox
label="Display Inactive Segmentations"
checked={config.renderInactiveSegmentations}
labelClassName="text-[12px] pt-1"
labelClassName="text-[12px]"
className="mb-[9px]"
onChange={setRenderInactiveSegmentations}
/>
@@ -94,7 +97,7 @@ const InactiveSegmentationConfig = ({
<InputRange
minValue={0}
maxValue={100}
value={config.fillAlphaInactive * 100}
value={getRoundedValue(config.fillAlphaInactive * 100)}
onChange={setFillAlphaInactive}
step={1}
containerClassName="mt-[4px]"
@@ -120,51 +123,37 @@ const SegmentationConfig = ({
const { initialConfig } = segmentationConfig;
const [isMinimized, setIsMinimized] = useState(true);
return (
<div className="bg-primary-dark">
<div
className="flex cursor-pointer items-center"
onClick={evt => {
evt.stopPropagation();
setIsMinimized(!isMinimized);
}}
>
<Icon
name="panel-group-open-close"
className={classNames('h-5 w-5 cursor-pointer text-white transition duration-300', {
'rotate-90 transform': !isMinimized,
})}
<div className="bg-primary-dark select-none">
<div>
<ActiveSegmentationConfig
config={initialConfig}
setFillAlpha={setFillAlpha}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderOutline={setRenderOutline}
/>
<span className="text-[12px] font-[300] text-[#d8d8d8]">{'Segmentation Appearance'}</span>
</div>
{/* active segmentation */}
{!isMinimized && (
<div>
<ActiveSegmentationConfig
config={initialConfig}
setFillAlpha={setFillAlpha}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderOutline={setRenderOutline}
<div className="mx-1 mb-[8px] h-[1px] bg-[#212456]"></div>
<div
onClick={() => setIsMinimized(!isMinimized)}
className="flex cursor-pointer items-center pl-2 pb-[9px]"
>
<Icon
name="panel-group-open-close"
className={classNames('h-5 w-5 cursor-pointer text-white transition duration-300', {
'rotate-90 transform': !isMinimized,
})}
/>
{/* A small line */}
<div className="mx-1 mb-[8px] h-[1px] bg-[#212456]"></div>
{/* inactive segmentation */}
<div
onClick={() => setIsMinimized(!isMinimized)}
className="flex cursor-pointer items-center pl-2 pb-[9px]"
>
<span className="text-[12px] font-[300] text-[#d8d8d8]">
{'Inactive Segmentations'}
</span>
</div>
<span className="text-[12px] font-[300] text-[#d8d8d8]">{'Inactive Segmentations'}</span>
</div>
{!isMinimized && (
<InactiveSegmentationConfig
config={initialConfig}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setFillAlphaInactive={setFillAlphaInactive}
/>
</div>
)}
)}
</div>
<div className="h-[6px] bg-black "></div>
</div>
);
@@ -0,0 +1,160 @@
import React from 'react';
import { Select, Icon, Dropdown } from '../../components';
import PropTypes from 'prop-types';
function SegmentationDropDownRow({
segmentations = [],
activeSegmentation,
onActiveSegmentationChange,
disableEditing,
onToggleSegmentationVisibility,
onSegmentationEdit,
onSegmentationDownload,
storeSegmentation,
onSegmentationDelete,
onSegmentationAdd,
}) {
const handleChange = option => {
onActiveSegmentationChange(option.value); // Notify the parent
};
const selectOptions = segmentations.map(s => ({
value: s.id,
label: s.label,
}));
if (!activeSegmentation) {
return null;
}
return (
<div className="group mx-0.5 mt-[8px] flex items-center">
<div
onClick={e => {
e.stopPropagation();
}}
>
<Dropdown
id="segmentation-dropdown"
showDropdownIcon={false}
alignment="left"
itemsClassName="text-primary-active"
showBorders={false}
list={[
...(!disableEditing
? [
{
title: 'Add New Segmentation',
onClick: () => {
onSegmentationAdd();
},
},
]
: []),
...(!disableEditing
? [
{
title: 'Rename',
onClick: () => {
onSegmentationEdit(activeSegmentation.id);
},
},
]
: []),
{
title: 'Delete',
onClick: () => {
onSegmentationDelete(activeSegmentation.id);
},
},
...(!disableEditing
? [
{
title: 'Export DICOM SEG',
onClick: () => {
storeSegmentation(activeSegmentation.id);
},
},
{
title: 'Download',
onClick: () => {
onSegmentationDownload(activeSegmentation.id);
},
},
]
: []),
]}
>
<div className="hover:bg-secondary-dark mx-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]">
<Icon name="icon-more-menu"></Icon>
</div>
</Dropdown>
</div>
{selectOptions?.length && (
<Select
id="segmentation-select"
isClearable={false}
onChange={handleChange}
components={{
DropdownIndicator: () => (
<Icon
name={'chevron-down-new'}
className="mr-2"
/>
),
}}
isSearchable={false}
options={selectOptions}
value={selectOptions?.find(o => o.value === activeSegmentation.id)}
className="text-aqua-pale h-[26px] w-1/2 text-[13px]"
/>
)}
<div className="flex items-center">
<div
className="hover:bg-secondary-dark ml-3 mr-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]"
onClick={() => onToggleSegmentationVisibility(activeSegmentation.id)}
>
{activeSegmentation.isVisible ? (
<Icon
name="row-shown"
className="text-primary-active"
/>
) : (
<Icon
name="row-hidden"
className="text-primary-active"
/>
)}
</div>
</div>
</div>
);
}
SegmentationDropDownRow.propTypes = {
segmentations: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
})
).isRequired,
activeSegmentation: PropTypes.shape({
id: PropTypes.string.isRequired,
isVisible: PropTypes.bool.isRequired,
}),
onActiveSegmentationChange: PropTypes.func.isRequired,
disableEditing: PropTypes.bool,
onToggleSegmentationVisibility: PropTypes.func,
onSegmentationEdit: PropTypes.func,
onSegmentationDownload: PropTypes.func,
storeSegmentation: PropTypes.func,
onSegmentationDelete: PropTypes.func,
onSegmentationAdd: PropTypes.func,
};
SegmentationDropDownRow.defaultProps = {
segmentations: [],
disableEditing: false,
};
export default SegmentationDropDownRow;
@@ -1,287 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import SegmentationGroupSegment from './SegmentationGroupSegment';
import Dropdown from '../Dropdown';
import classnames from 'classnames';
import Icon from '../Icon';
import IconButton from '../IconButton';
const AddNewSegmentRow = ({
id,
onConfigChange,
onToggleSegmentationVisibility,
onSegmentAdd,
isVisible,
showAddSegment,
disableEditing,
}) => {
return (
<div>
<div className="text-primary-active flex cursor-pointer items-center bg-black py-1 pl-[29px] text-[12px] hover:opacity-80">
{showAddSegment && !disableEditing && (
<div
className="flex items-center"
onClick={() => onSegmentAdd()}
>
<Icon
name="row-add"
className="h-5 w-5"
/>
<div className="">Add Segment</div>
</div>
)}
<div className="flex-grow" />
<div
className="flex items-center pr-2"
onClick={() => {
onToggleSegmentationVisibility(id);
}}
>
{isVisible ? (
<Icon
name="row-show-all"
className="h-5 w-5"
/>
) : (
<Icon
name="row-hide-all"
className="h-5 w-5"
/>
)}
</div>
</div>
{/* {isSegmentationConfigOpen && (
<SegmentationConfig onConfigChange={onConfigChange} />
)} */}
</div>
);
};
const SegmentGroupHeader = ({
isMinimized,
onToggleMinimizeSegmentation,
onSegmentationClick,
id,
label,
isActive,
segmentCount,
onSegmentationEdit,
disableEditing,
onSegmentationDelete,
}) => {
return (
<div
className={classnames(
'flex-static border-secondary-light flex h-[27px] cursor-pointer items-center gap-2 rounded-t-md border-b pr-2 pl-[3px] text-[12px]',
{
'bg-secondary-main': isActive,
'bg-secondary-dark': !isActive,
}
)}
onClick={evt => {
evt.stopPropagation();
onSegmentationClick(id);
}}
>
<Icon
name="panel-group-open-close"
onClick={evt => {
evt.stopPropagation();
onToggleMinimizeSegmentation(id);
}}
className={classnames('h-5 w-5 cursor-pointer text-white transition duration-300', {
'rotate-90 transform': !isMinimized,
})}
/>
<span className="w-full overflow-hidden text-ellipsis whitespace-nowrap text-white">
{label.toUpperCase()}
</span>
<div className="flex-grow" />
<span className="text-white ">{segmentCount}</span>
<div
onClick={e => {
e.stopPropagation();
}}
>
{!disableEditing && (
<Dropdown
id="segmentation-dropdown"
showDropdownIcon={false}
list={[
{
title: 'Rename',
onClick: () => {
onSegmentationEdit(id);
},
},
{
title: 'Delete',
onClick: () => {
onSegmentationDelete(id);
},
},
]}
>
<IconButton
id={''}
variant="text"
color="inherit"
size="initial"
className="text-primary-active"
>
<Icon name="panel-group-more" />
</IconButton>
</Dropdown>
)}
</div>
</div>
);
};
const SegmentationGroup = ({
label,
id,
segmentCount,
isVisible,
isActive,
onSegmentClick,
isMinimized,
onSegmentColorClick,
disableEditing,
showAddSegment,
segments,
activeSegmentIndex,
onSegmentAdd,
onSegmentationClick,
onClickSegmentColor,
onSegmentationEdit,
onToggleSegmentVisibility,
onToggleSegmentationVisibility,
onSegmentDelete,
showSegmentDelete,
onToggleMinimizeSegmentation,
onSegmentationConfigChange,
onSegmentationDelete,
onSegmentEdit,
}) => {
return (
<div className="flex min-h-0 flex-auto flex-col">
<SegmentGroupHeader
id={id}
label={label}
isActive={isActive}
isMinimized={isMinimized}
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
onSegmentationClick={onSegmentationClick}
segmentCount={segmentCount}
onSegmentationEdit={onSegmentationEdit}
onSegmentationDelete={onSegmentationDelete}
disableEditing={disableEditing}
/>
{!isMinimized && (
<div className="flex min-h-0 flex-auto flex-col">
<AddNewSegmentRow
onConfigChange={onSegmentationConfigChange}
onSegmentAdd={onSegmentAdd}
isVisible={isVisible}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
id={id}
disableEditing={disableEditing}
showAddSegment={showAddSegment}
/>
<div className="ohif-scrollbar flex min-h-0 flex-col overflow-y-hidden">
{!!segments.length &&
segments.map(segment => {
if (segment === undefined || segment === null) {
return null;
}
const { segmentIndex, color, label, isVisible, isLocked } = segment;
return (
<div
className="mb-[1px]"
key={segmentIndex}
>
<SegmentationGroupSegment
segmentationId={id}
segmentIndex={segmentIndex}
label={label}
color={color}
isActive={activeSegmentIndex === segmentIndex}
isLocked={isLocked}
isVisible={isVisible}
disableEditing={disableEditing}
onClick={onSegmentClick}
onEdit={onSegmentEdit}
onDelete={onSegmentDelete}
showSegmentDelete={showSegmentDelete}
onColor={onSegmentColorClick}
onToggleVisibility={onToggleSegmentVisibility}
/>
</div>
);
})}
</div>
</div>
)}
</div>
);
};
SegmentationGroup.propTypes = {
label: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
segments: PropTypes.array.isRequired,
segmentCount: PropTypes.number.isRequired,
isVisible: PropTypes.bool.isRequired,
isActive: PropTypes.bool.isRequired,
isMinimized: PropTypes.bool.isRequired,
activeSegmentIndex: PropTypes.number,
onClickNewSegment: PropTypes.func.isRequired,
onClickSegment: PropTypes.func.isRequired,
onClickSegmentEdit: PropTypes.func.isRequired,
onClickSegmentDelete: PropTypes.func.isRequired,
onToggleSegmentLocked: PropTypes.func,
onToggleSegmentVisibility: PropTypes.func.isRequired,
onToggleSegmentationVisibility: PropTypes.func.isRequired,
onSegmentClick: PropTypes.func.isRequired,
showAddSegment: PropTypes.bool.isRequired,
onSegmentAdd: PropTypes.func.isRequired,
onSegmentationClick: PropTypes.func.isRequired,
onClickSegmentColor: PropTypes.func.isRequired,
onSegmentationEdit: PropTypes.func.isRequired,
onSegmentDelete: PropTypes.func.isRequired,
onToggleMinimizeSegmentation: PropTypes.func.isRequired,
onSegmentationConfigChange: PropTypes.func.isRequired,
onSegmentationDelete: PropTypes.func.isRequired,
onSegmentEdit: PropTypes.func.isRequired,
disableEditing: PropTypes.bool,
};
SegmentationGroup.defaultProps = {
label: '',
segmentCount: 0,
segments: [],
isVisible: true,
isMinimized: false,
onClickNewSegment: () => {},
onClickSegment: () => {},
onClickSegmentEdit: () => {},
onClickSegmentDelete: () => {},
onToggleSegmentLocked: () => {},
onToggleSegmentVisibility: () => {},
onToggleSegmentationVisibility: () => {},
onSegmentClick: () => {},
showAddSegment: false,
onSegmentAdd: () => {},
onSegmentationClick: () => {},
onClickSegmentColor: () => {},
onSegmentationEdit: () => {},
onSegmentDelete: () => {},
onToggleMinimizeSegmentation: () => {},
onSegmentationConfigChange: () => {},
onSegmentationDelete: () => {},
onSegmentEdit: () => {},
};
export default SegmentationGroup;
@@ -11,7 +11,7 @@ const SegmentItem = ({
isActive,
isVisible,
color,
showSegmentDelete,
showDelete,
disableEditing,
isLocked = false,
onClick,
@@ -21,200 +21,168 @@ const SegmentItem = ({
onToggleVisibility,
onToggleLocked,
}) => {
const [isHovering, setIsHovering] = useState(false);
const [isSegmentIndexHovering, setIsSegmentIndexHovering] = useState(false);
const onMouseEnter = () => setIsHovering(true);
const onMouseLeave = () => setIsHovering(false);
const [isNumberBoxHovering, setIsNumberBoxHovering] = useState(false);
const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`;
return (
<div
className={classnames(
'bg-primary-dark group relative flex cursor-pointer overflow-hidden border text-[12px] transition duration-300',
{
'border-primary-light rounded-[3px]': isActive,
},
{
'border-transparent': !isActive,
}
)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={classnames('text-aqua-pale group/row flex min-h-[28px] bg-black')}
onClick={e => {
e.stopPropagation();
onClick(segmentationId, segmentIndex);
}}
role="button"
tabIndex={0}
data-cy={'segment-item'}
>
<div
className={classnames(
'flex w-[27px] items-center justify-center border-r border-r-black text-[12px]',
{
'bg-primary-light border-l-primary-light rounded-l-sm border-l text-black': isActive,
'rounded-l-sm border-l border-l-[#1d3c58] bg-[#1d3c58]': !isActive && isHovering,
'bg-primary-dark text-aqua-pale': !isActive && !isHovering && isVisible,
'bg-[#140e2e] text-[#537594] opacity-60': !isActive && !isVisible,
}
)}
onMouseEnter={() => setIsSegmentIndexHovering(true)}
onMouseLeave={() => setIsSegmentIndexHovering(false)}
className={classnames('bg-primary-dark group/number grid w-[32px] place-items-center', {
'bg-primary-light border-primary-light rounded-l-[4px] border text-black': isActive,
'border-primary-dark border': !isActive,
})}
onMouseEnter={() => setIsNumberBoxHovering(true)}
onMouseLeave={() => setIsNumberBoxHovering(false)}
>
{isSegmentIndexHovering && showSegmentDelete ? (
{isNumberBoxHovering && showDelete ? (
<Icon
name="close"
className={classnames('pr-0.5')}
className={classnames('h-[8px] w-[8px]', {
'hover:cursor-pointer hover:opacity-60': !disableEditing,
})}
onClick={e => {
if (disableEditing) {
return;
}
e.stopPropagation();
onDelete(segmentationId, segmentIndex);
}}
/>
) : (
<div className={classnames('flex items-center pr-2 ')}>{segmentIndex}</div>
<div>{segmentIndex}</div>
)}
</div>
<div
className={classnames(
'relative flex w-full items-center justify-between border-r border-r-black py-1 pl-2 text-white ',
{
'bg-primary-dark text-primary-light': isActive,
'bg-primary-dark text-aqua-pale': !isActive && isVisible,
'bg-[#140e2e] text-[#3d5871] opacity-60': !isVisible,
}
)}
className={classnames('relative flex w-full', {
'border-primary-light bg-primary-dark rounded-r-[4px] border border-l-0': isActive,
'border border-l-0 border-transparent': !isActive,
})}
>
<div className={classnames('flex items-baseline gap-2')}>
<div
className={classnames('h-[8px] w-[8px] shrink-0 rounded-full')}
style={{ backgroundColor: cssColor }}
onClick={e => {
e.stopPropagation();
onColor(segmentationId, segmentIndex);
}}
/>
<div className="">{label}</div>
</div>
{/* with faded background */}
<div className="bg-black/15 absolute right-0 rounded-lg pr-[7px]">
{!isVisible && !isHovering && (
<div>
<Icon
name="row-hidden"
className={classnames('h-5 w-5 text-[#3d5871] ')}
onClick={e => {
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
</div>
)}
{isHovering && (
<div className={classnames('flex items-center')}>
{!disableEditing && (
<Icon
name="row-edit"
className={classnames('h-5 w-5', {
'text-white': isLocked,
'text-primary-light': !isLocked,
})}
onClick={e => {
e.stopPropagation();
onEdit(segmentationId, segmentIndex);
}}
/>
)}
{isVisible ? (
<Icon
name="row-hide"
className={classnames('h-5 w-5', {
'text-white': isLocked,
'text-primary-light': !isLocked,
})}
onClick={e => {
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
) : (
<Icon
name="row-unhide"
className={classnames('h-5 w-5', {
'text-white': isLocked,
'text-primary-light': !isLocked,
})}
onClick={e => {
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
)}
</div>
)}
</div>
</div>
{/* <div className="relative flex flex-col w-full border-t border-t-black">
<div className="fl`ex items-center mb-1 ml-2">
<div className="flex items-center flex-1 text-base text-primary-light">
<div className="group-hover/row:bg-primary-dark flex h-full w-full flex-grow items-center">
<div className="pl-2 pr-1.5">
<div
className={classnames(
'w-3 h-3 mt-1 mr-2 rounded-full cursor-pointer transition duration-300 hover:opacity-80'
)}
className={classnames('h-[8px] w-[8px] grow-0 rounded-full', {
'hover:cursor-pointer hover:opacity-60': !disableEditing,
})}
style={{ backgroundColor: cssColor }}
onClick={e => {
if (disableEditing) {
return;
}
e.stopPropagation();
onColor(segmentationId, segmentIndex);
}}
style={{ backgroundColor: cssColor }}
></div>
{label}
/>
</div>
<div className="flex items-center w-1/3">
<div className="px-1">
<div className="flex items-center py-1 hover:cursor-pointer">{label}</div>
</div>
<div
className={classnames(
'absolute right-3 top-0 flex flex-row-reverse rounded-lg pt-[3px]',
{}
)}
>
<div className="group-hover/row:hidden">
{!isVisible && (
<Icon
className={classnames(
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
)}
name={isVisible ? 'eye-visible' : 'eye-hidden'}
name="row-hidden"
className="h-5 w-5 text-[#3d5871]"
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
</div>
{onToggleLocked !== undefined ? (
<div className="px-1">
)}
</div>
{/* Icon for 'row-lock' that shows when NOT hovering and 'isLocked' is true */}
<div className="group-hover/row:hidden">
{isLocked && (
<div className="flex">
<Icon
className={classnames(
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
)}
name={isLocked ? 'lock' : 'dotted-circle'}
name="row-lock"
className="h-5 w-5 text-[#3d5871]"
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
onToggleLocked(segmentationId, segmentIndex);
}}
/>
</div>
) : null}
<div className="px-1">
<Icon
className={classnames(
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
{/* This icon is visible when 'isVisible' is true */}
{isVisible && (
<Icon
name="row-hidden"
className="h-5 w-5 opacity-0"
/>
)}
name={'pencil'}
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
onEdit(segmentationId, segmentIndex);
}}
/>
</div>
</div>
)}
</div>
{/* Icons that show only when hovering */}
<div className="hidden group-hover/row:flex">
<HoveringIcons
onEdit={onEdit}
isLocked={isLocked}
isVisible={isVisible}
onToggleLocked={onToggleLocked}
onToggleVisibility={onToggleVisibility}
segmentationId={segmentationId}
segmentIndex={segmentIndex}
/>
</div>
</div>
</div> */}
</div>
</div>
);
};
const HoveringIcons = ({
onEdit,
isLocked,
isVisible,
onToggleLocked,
onToggleVisibility,
segmentationId,
segmentIndex,
}) => {
const iconClass = 'w-5 h-5 hover:cursor-pointer hover:opacity-60';
const handleIconClick = (e, action) => {
e.stopPropagation();
action(segmentationId, segmentIndex);
};
const createIcon = (name, action, color = null) => (
<Icon
name={name}
className={classnames(iconClass, color ?? 'text-white')}
onClick={e => handleIconClick(e, action)}
/>
);
return (
<div className="flex items-center">
{createIcon('row-edit', onEdit)}
{createIcon(
isLocked ? 'row-lock' : 'row-unlock',
onToggleLocked,
isLocked ? 'text-[#3d5871]' : null
)}
{createIcon(
isVisible ? 'row-shown' : 'row-hidden',
onToggleVisibility,
!isVisible ? 'text-[#3d5871]' : null
)}
</div>
);
};
@@ -1,28 +1,38 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Icon from '../Icon';
import SegmentationGroup from './SegmentationGroup';
import { PanelSection } from '../../components';
import SegmentationConfig from './SegmentationConfig';
import SegmentationDropDownRow from './SegmentationDropDownRow';
import NoSegmentationRow from './NoSegmentationRow';
import AddSegmentRow from './AddSegmentRow';
import SegmentationGroupSegment from './SegmentationGroupSegment';
const SegmentationGroupTable = ({
segmentations,
// segmentation initial config
segmentationConfig,
// UI show/hide
disableEditing,
showAddSegmentation,
showAddSegment,
showDeleteSegment,
// segmentation/segment handlers
onSegmentationAdd,
onSegmentationEdit,
onSegmentationClick,
onSegmentationDelete,
showAddSegmentation,
showAddSegment,
onSegmentationDownload,
storeSegmentation,
// segment handlers
onSegmentClick,
onSegmentAdd,
segmentationConfig,
disableEditing,
onSegmentDelete,
onSegmentEdit,
onToggleSegmentationVisibility,
onToggleSegmentVisibility,
onToggleSegmentLock,
onSegmentColorClick,
isMinimized,
onToggleMinimizeSegmentation,
// segmentation config handlers
setFillAlpha,
setFillAlphaInactive,
setOutlineWidthActive,
@@ -31,111 +41,191 @@ const SegmentationGroupTable = ({
setRenderInactiveSegmentations,
setRenderOutline,
}) => {
const [isConfigOpen, setIsConfigOpen] = useState(false);
const [activeSegmentationId, setActiveSegmentationId] = useState(null);
const onActiveSegmentationChange = segmentationId => {
onSegmentationClick(segmentationId);
setActiveSegmentationId(segmentationId);
};
useEffect(() => {
// find the first active segmentation to set
let activeSegmentationIdToSet = segmentations?.find(segmentation => segmentation.isActive)?.id;
// If there is no active segmentation, set the first one to be active
if (!activeSegmentationIdToSet && segmentations?.length > 0) {
activeSegmentationIdToSet = segmentations[0].id;
}
// If there is no segmentation, set the active segmentation to null
if (segmentations?.length === 0) {
activeSegmentationIdToSet = null;
}
setActiveSegmentationId(activeSegmentationIdToSet);
}, [segmentations]);
const activeSegmentation = segmentations?.find(
segmentation => segmentation.id === activeSegmentationId
);
return (
<div className="font-inter flex min-h-0 flex-col font-[300]">
<SegmentationConfig
setFillAlpha={setFillAlpha}
setFillAlphaInactive={setFillAlphaInactive}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setRenderOutline={setRenderOutline}
segmentationConfig={segmentationConfig}
/>
<div className="mt-1 flex min-h-0 flex-col pr-[1px]">
{!!segmentations.length &&
segmentations.map((segmentation, index) => {
const {
id,
label,
displayText = [],
segmentCount,
segments,
isVisible,
isActive,
activeSegmentIndex,
} = segmentation;
return (
<SegmentationGroup
id={id}
key={id}
label={label}
disableEditing={disableEditing}
isMinimized={isMinimized[id]}
segments={segments}
showAddSegment={showAddSegment}
segmentCount={segmentCount}
isActive={isActive}
isVisible={isVisible}
onSegmentColorClick={onSegmentColorClick}
onSegmentationClick={() => onSegmentationClick(id)}
activeSegmentIndex={activeSegmentIndex}
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
onSegmentationEdit={onSegmentationEdit}
onSegmentationDelete={onSegmentationDelete}
onSegmentClick={onSegmentClick}
onSegmentEdit={onSegmentEdit}
onToggleSegmentVisibility={onToggleSegmentVisibility}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
onSegmentAdd={onSegmentAdd}
showSegmentDelete={false}
/>
);
})}
</div>
{showAddSegmentation && !disableEditing && (
<div
className="text-primary-active flex h-[45px] cursor-pointer items-center bg-black pl-1 text-[12px] hover:opacity-80"
onClick={() => onSegmentationAdd()}
>
<Icon
name="row-add"
className="h-5 w-5"
<div className="flex min-h-0 flex-col bg-black text-[13px] font-[300]">
<PanelSection
title="Segmentation"
actionIcons={
activeSegmentation && [
{
name: 'settings-bars',
onClick: () => setIsConfigOpen(isOpen => !isOpen),
},
]
}
>
{isConfigOpen && (
<SegmentationConfig
setFillAlpha={setFillAlpha}
setFillAlphaInactive={setFillAlphaInactive}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setRenderOutline={setRenderOutline}
segmentationConfig={segmentationConfig}
/>
<div className="pl-1">Add New Segmentation</div>
)}
<div className="bg-primary-dark">
{segmentations?.length === 0 ? (
<div className="select-none rounded-[4px]">
{showAddSegmentation && !disableEditing && (
<NoSegmentationRow onSegmentationAdd={onSegmentationAdd} />
)}
</div>
) : (
<div className="mt-1 select-none">
<SegmentationDropDownRow
segmentations={segmentations}
disableEditing={disableEditing}
activeSegmentation={activeSegmentation}
onActiveSegmentationChange={onActiveSegmentationChange}
onSegmentationDelete={onSegmentationDelete}
onSegmentationEdit={onSegmentationEdit}
onSegmentationDownload={onSegmentationDownload}
storeSegmentation={storeSegmentation}
onSegmentationAdd={onSegmentationAdd}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
/>
{!disableEditing && showAddSegment && (
<AddSegmentRow onClick={() => onSegmentAdd(activeSegmentationId)} />
)}
</div>
)}
</div>
)}
{activeSegmentation && (
<div className="ohif-scrollbar mt-1.5 flex min-h-0 flex-col overflow-y-hidden">
{activeSegmentation?.segments?.map(segment => {
if (!segment) {
return null;
}
const { segmentIndex, color, label, isVisible, isLocked } = segment;
return (
<div
className="mb-[1px]"
key={segmentIndex}
>
<SegmentationGroupSegment
segmentationId={activeSegmentationId}
segmentIndex={segmentIndex}
label={label}
color={color}
isActive={activeSegmentation.activeSegmentIndex === segmentIndex}
disableEditing={disableEditing}
isLocked={isLocked}
isVisible={isVisible}
onClick={onSegmentClick}
onEdit={onSegmentEdit}
onDelete={onSegmentDelete}
showDelete={showDeleteSegment}
onColor={onSegmentColorClick}
onToggleVisibility={onToggleSegmentVisibility}
onToggleLocked={onToggleSegmentLock}
/>
</div>
);
})}
</div>
)}
</PanelSection>
</div>
);
};
SegmentationGroupTable.propTypes = {
title: PropTypes.string.isRequired,
segmentations: PropTypes.array.isRequired,
activeSegmentationId: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onToggleLocked: PropTypes.func,
onToggleVisibility: PropTypes.func.isRequired,
onToggleVisibilityAll: PropTypes.func.isRequired,
segmentationConfig: PropTypes.object,
segmentations: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired,
segments: PropTypes.arrayOf(
PropTypes.shape({
segmentIndex: PropTypes.number.isRequired,
color: PropTypes.array.isRequired,
label: PropTypes.string.isRequired,
isVisible: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired,
})
),
})
),
segmentationConfig: PropTypes.object.isRequired,
disableEditing: PropTypes.bool,
showAddSegmentation: PropTypes.bool,
showAddSegment: PropTypes.bool,
showDeleteSegment: PropTypes.bool,
onSegmentationAdd: PropTypes.func.isRequired,
onSegmentationEdit: PropTypes.func.isRequired,
onSegmentationClick: PropTypes.func.isRequired,
onSegmentationDelete: PropTypes.func.isRequired,
onSegmentationDownload: PropTypes.func.isRequired,
storeSegmentation: PropTypes.func.isRequired,
onSegmentClick: PropTypes.func.isRequired,
onSegmentAdd: PropTypes.func.isRequired,
onSegmentDelete: PropTypes.func.isRequired,
onSegmentEdit: PropTypes.func.isRequired,
onToggleSegmentationVisibility: PropTypes.func.isRequired,
onToggleSegmentVisibility: PropTypes.func.isRequired,
onToggleSegmentLock: PropTypes.func.isRequired,
onSegmentColorClick: PropTypes.func.isRequired,
setFillAlpha: PropTypes.func.isRequired,
setFillAlphaInactive: PropTypes.func.isRequired,
setOutlineWidthActive: PropTypes.func.isRequired,
setOutlineOpacityActive: PropTypes.func.isRequired,
setRenderFill: PropTypes.func.isRequired,
setRenderInactiveSegmentations: PropTypes.func.isRequired,
setRenderOutline: PropTypes.func.isRequired,
};
SegmentationGroupTable.defaultProps = {
title: '',
segmentations: [],
activeSegmentationId: '',
onClick: () => {},
onEdit: () => {},
onDelete: () => {},
onToggleLocked: () => {},
onToggleVisibility: () => {},
onToggleVisibilityAll: () => {},
segmentationConfig: {
initialConfig: {
fillAlpha: 0.5,
fillAlphaInactive: 0.5,
outlineWidthActive: 1,
outlineOpacity: 1,
outlineOpacityInactive: 0.5,
renderFill: true,
renderInactiveSegmentations: true,
renderOutline: true,
},
},
disableEditing: false,
showAddSegmentation: true,
showAddSegment: true,
showDeleteSegment: true,
onSegmentationAdd: () => {},
onSegmentationEdit: () => {},
onSegmentationClick: () => {},
onSegmentationDelete: () => {},
onSegmentationDownload: () => {},
storeSegmentation: () => {},
onSegmentClick: () => {},
onSegmentAdd: () => {},
onSegmentDelete: () => {},
onSegmentEdit: () => {},
onToggleSegmentationVisibility: () => {},
onToggleSegmentVisibility: () => {},
onToggleSegmentLock: () => {},
onSegmentColorClick: () => {},
setFillAlpha: () => {},
setFillAlphaInactive: () => {},
setOutlineWidthActive: () => {},
@@ -144,5 +234,4 @@ SegmentationGroupTable.defaultProps = {
setRenderInactiveSegmentations: () => {},
setRenderOutline: () => {},
};
export default SegmentationGroupTable;
@@ -1,7 +1,5 @@
import SegmentationGroupTable from '../SegmentationGroupTable';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
import { SegmentationGroupTable } from '../../../components';
export const argTypes = {
component: SegmentationGroupTable,
@@ -40,13 +38,14 @@ SegmentationGroupTable is a component that renders the SegmentationGroupTables.
showAddSegmentation: true,
isMinimized: { '4be5-b9b2-ed33-fa86970568e7': false },
showAddSegment: true,
showDeleteSegment: true,
segmentations: [
{
id: '4be5-b9b2-ed33-fa86970568e7',
volumeId: 'a3b0f15f-4be5-b9b2-ed33-fa86970568e7',
activeSegmentIndex: 1,
activeSegmentIndex: 2,
cachedStats: {},
label: 'Segmentation',
label: 'Segmentation 1',
segmentsLocked: {},
type: 'LABELMAP',
displayText: [],
@@ -55,7 +54,7 @@ SegmentationGroupTable is a component that renders the SegmentationGroupTables.
segments: [
null,
{
label: 'Segment 1',
label: 'This is a very long segment label',
segmentIndex: 1,
color: [128, 174, 128],
opacity: 255,
@@ -88,7 +87,7 @@ SegmentationGroupTable is a component that renders the SegmentationGroupTables.
volumeId: 'a3b0f15f-4be5-b9b2-ed33-fa86970568e7',
activeSegmentIndex: 1,
cachedStats: {},
label: 'Segmentation2',
label: '2 Segmentation',
segmentsLocked: {},
type: 'LABELMAP',
displayText: [],
+19 -5
View File
@@ -1,6 +1,6 @@
.customSelect__wrapper .customSelect__control {
@apply border-primary-main w-full rounded border bg-black text-sm leading-tight text-white shadow transition duration-300;
min-height: 33px;
@apply border-secondary-light w-full rounded border bg-black text-sm leading-tight text-white shadow transition duration-300;
min-height: 28px;
}
.customSelect__wrapper .customSelect__control:hover {
@apply border-gray-500;
@@ -15,6 +15,10 @@
@apply pointer-events-none;
}
.css-1hwfws3 {
padding: 0;
}
.customSelect__wrapper .customSelect__indicator-separator {
@apply hidden;
}
@@ -36,7 +40,11 @@
}
.customSelect__wrapper .customSelect__single-value {
@apply text-white;
@apply text-aqua-pale pl-1;
max-width: calc(100% - 20px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.customSelect__wrapper.customSelect--is-disabled {
@@ -54,8 +62,7 @@
.ohif-select .customSelect__menu-list {
overflow-x: hidden !important;
margin-right: 4px;
margin-top: 4px;
margin: 4px;
padding-top: 0px !important;
}
@@ -63,6 +70,9 @@
width: 6px;
border-radius: 10px;
}
.ohif-select .customSelect__placeholder {
@apply text-aqua-pale pl-1.5;
}
.ohif-select .customSelect__menu-list::-webkit-scrollbar-thumb {
background-color: #0944b3;
@@ -90,3 +100,7 @@
border-radius: 6px;
background-color: #151515 !important;
}
.css-1hwfws3 {
all: unset !important;
}
+7 -2
View File
@@ -54,6 +54,7 @@ const Select = ({
placeholder,
noIcons,
menuPlacement,
components,
value,
}) => {
const _noIconComponents = {
@@ -61,7 +62,10 @@ const Select = ({
IndicatorSeparator: () => null,
};
let _components = isMulti ? { Option, MultiValue } : {};
_components = noIcons ? { ..._components, ..._noIconComponents } : _components;
_components = noIcons
? { ..._components, ..._noIconComponents }
: { ..._components, ...components };
const selectedOptions = [];
// Map array of values to an array of selected options
@@ -104,8 +108,9 @@ const Select = ({
Select.defaultProps = {
className: '',
closeMenuOnSelect: true,
hideSelectedOptions: true,
hideSelectedOptions: false,
isClearable: true,
components: {},
isDisabled: false,
isMulti: false,
isSearchable: true,
@@ -0,0 +1,56 @@
import { Select } from '../../../components';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
component: Select,
title: 'Components/Select',
};
<Meta
title="Components/Select"
component={Select}
/>
export const SelectTemplate = args => (
<div className="h-80 w-40">
<Select {...args} />
</div>
);
<Heading
title="Select"
componentRelativePath="Select/Select.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
Select is a component that allows you to select from a list
<Canvas>
<Story
name="Overview"
args={{
isClearable: false,
onChange: item => alert(item.label),
options: [
{ label: 'item1', value: 1 },
{ label: 'item2', value: 2 },
],
value: 2,
}}
>
{SelectTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={Select} />
## Contribute
<Footer componentRelativePath="Select/__stories__/Select.stories.mdx" />
@@ -207,10 +207,7 @@ const SidePanel = ({
<React.Fragment>
{/** Panel Header with Arrow and Close Actions */}
<div
className={classnames(
'flex-static bg-primary-dark flex h-9 cursor-pointer px-[10px]',
tabs.length === 1 && 'mb-1'
)}
className={classnames('flex-static bg-primary-dark flex h-9 cursor-pointer px-[10px]')}
onClick={() => {
updatePanelOpen(prev => !prev);
// slideToActivePanel();
@@ -7,13 +7,12 @@ import { useTranslation } from 'react-i18next';
import Icon from '../Icon';
import Tooltip from '../Tooltip';
import ListMenu from '../ListMenu';
import ToolbarButton from '../ToolbarButton';
const baseClasses = {
Button: 'flex items-center rounded-md border-transparent cursor-pointer group/button',
Primary:
// By default border on left, top and bottom for hover effect and only rounded on left side.
// Extra padding on right to componensate for no right border.
// Extra padding on right to compensate for no right border.
'h-full border-l-2 border-t-2 border-b-2 rounded-tl-md rounded-bl-md group/primary !pl-2 !py-2',
Secondary:
'h-full flex items-center justify-center rounded-tr-md rounded-br-md w-4 border-2 border-transparent group/secondary',
@@ -72,154 +71,57 @@ const classes = {
};
const SplitButton = ({
isRadio,
isAction,
//
bState,
//
isToggle,
groupId,
primary: _primary,
primary,
secondary,
items: _items,
items,
renderer,
isActive,
onInteraction,
servicesManager,
Component,
}) => {
const { t } = useTranslation('Buttons');
const [state, setState] = useState({ isHovering: false, isExpanded: false });
const { toolbarService } = servicesManager?.services || {};
const toggleExpanded = () => setState({ ...state, isExpanded: !state.isExpanded });
const setHover = hovering => setState({ ...state, isHovering: hovering });
const collapse = () => setState({ ...state, isExpanded: false });
const { primaryToolId, toggles } = bState;
/* Bubbles up individual item clicks */
const getSplitButtonItems = items =>
items.map((item, index) => ({
...item,
index,
onClick: () => {
onInteraction({
groupId,
//
itemId: item.id,
interactionType: item.type,
// splitButtonId? (so we can track group?)
// info to fire item's command/event?
commands: item.commands,
});
setState(state => ({
...state,
primary: !isAction && isRadio ? { ...item, index } : state.primary,
isExpanded: false,
items: getSplitButtonItems(_items).filter(item =>
isRadio && !isAction ? item.index !== index : true
),
}));
},
}));
const [state, setState] = useState({
primary: _primary,
items: getSplitButtonItems(_items).filter(item =>
isRadio && !isAction ? item.id !== _primary.id : true
),
isHovering: false,
isExpanded: false,
});
const onSecondaryClickHandler = () =>
setState(state => ({ ...state, isExpanded: !state.isExpanded }));
const onMouseEnterHandler = () => setState(state => ({ ...state, isHovering: true }));
const onMouseLeaveHandler = () => setState(state => ({ ...state, isHovering: false }));
const outsideClickHandler = () => setState(state => ({ ...state, isExpanded: false }));
const isPrimaryToggle = state.primary.type === 'toggle';
const isPrimaryActive =
(state.primary.type === 'tool' && primaryToolId === state.primary.id) ||
(isPrimaryToggle && toggles[state.primary.id] === true);
const PrimaryButtonComponent =
toolbarService?.getButtonComponentForUIType(state.primary.uiType) ?? ToolbarButton;
const primaryButtonClassName = classes.Primary({
...state,
primary: { isActive: isPrimaryActive, isToggle: isPrimaryToggle },
});
const DefaultListItemRenderer = ({ type, icon, label, t, id }) => {
const isActive = type === 'toggle' && toggles[id] === true;
return (
<div
className={classNames(
'hover:bg-primary-dark flex h-8 w-full flex-row items-center p-3',
'whitespace-pre text-base',
isActive && 'bg-primary-dark',
isActive
? 'text-[#348CFD]'
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light'
)}
>
{icon && (
<span className="mr-4">
<Icon
name={icon}
className="h-5 w-5"
/>
</span>
)}
<span className="mr-5">{t(label)}</span>
</div>
);
};
const listItemRenderer = renderer || DefaultListItemRenderer;
const renderPrimaryButton = () => (
<Component
key={primary.id}
{...primary}
isActive={isActive}
onInteraction={onInteraction}
rounded="none"
className={classes.Primary({ ...state, primary: { isActive, isToggle } })}
data-tool={primary.id}
data-cy={`${groupId}-split-button-primary`}
/>
);
return (
<OutsideClickHandler
onOutsideClick={outsideClickHandler}
onOutsideClick={collapse}
disabled={!state.isExpanded}
>
<div
name="SplitButton"
id="SplitButton"
className="relative"
>
<div
className={classes.Button({
...state,
primary: { isActive: isPrimaryActive },
})}
className={classes.Button({ ...state, primary: { isActive } })}
style={{ height: '40px' }}
onMouseEnter={onMouseEnterHandler}
onMouseLeave={onMouseLeaveHandler}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
<div className={classes.Interface}>
<div onClick={outsideClickHandler}>
<PrimaryButtonComponent
key={state.primary.id}
{...state.primary}
bState={bState}
isActive={isPrimaryActive}
onInteraction={args => toolbarService.recordInteraction(args)}
servicesManager={servicesManager}
// All rounding is taken care of by className
rounded="none"
className={primaryButtonClassName}
data-tool={state.primary.id}
data-cy={`${groupId}-split-button-primary`}
/>
</div>
<div onClick={collapse}>{renderPrimaryButton()}</div>
<div className={classes.Separator({ ...state, primary: { isActive } })}></div>
<div
className={classes.Separator({
...state,
primary: { isActive: isPrimaryActive },
})}
></div>
<div
className={classes.Secondary({
...state,
primary: { isActive: isPrimaryActive },
})}
onClick={onSecondaryClickHandler}
className={classes.Secondary({ ...state, primary: { isActive } })}
onClick={toggleExpanded}
data-cy={`${groupId}-split-button-secondary`}
>
<Tooltip
@@ -229,24 +131,20 @@ const SplitButton = ({
>
<Icon
name={secondary.icon}
className={classes.SecondaryIcon({
...state,
primary: { isActive: isPrimaryActive },
})}
className={classes.SecondaryIcon({ ...state, primary: { isActive } })}
/>
</Tooltip>
</div>
</div>
</div>
{/* EXPANDED LIST OF OPTIONS */}
<div
className={classes.Content({ ...state })}
data-cy={`${groupId}-list-menu`}
>
<ListMenu
items={state.items}
bState={bState}
renderer={args => listItemRenderer({ ...args, t })}
items={items}
onClick={collapse}
renderer={args => renderer({ ...args, t })}
/>
</div>
</div>
@@ -254,51 +152,23 @@ const SplitButton = ({
);
};
SplitButton.defaultProps = {
isRadio: false,
isAction: false,
primary: {
label: null,
tooltip: null,
},
secondary: {
icon: 'chevron-down',
label: null,
isActive: true,
tooltip: 'More Measure Tools',
},
items: [],
renderer: null,
SplitButton.propTypes = {
isToggle: PropTypes.bool,
groupId: PropTypes.string.isRequired,
primary: PropTypes.object.isRequired,
secondary: PropTypes.object.isRequired,
items: PropTypes.array.isRequired,
renderer: PropTypes.func,
isActive: PropTypes.bool,
onInteraction: PropTypes.func.isRequired,
Component: PropTypes.elementType,
};
SplitButton.propTypes = {
primary: PropTypes.shape({
id: PropTypes.string.isRequired,
icon: PropTypes.string,
label: PropTypes.string,
type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired,
tooltip: PropTypes.string,
}),
secondary: PropTypes.shape({
id: PropTypes.string,
icon: PropTypes.string,
label: PropTypes.string,
tooltip: PropTypes.string,
isActive: PropTypes.bool,
}),
renderer: PropTypes.func,
items: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
icon: PropTypes.string,
label: PropTypes.string,
type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired,
tooltip: PropTypes.string,
isActive: PropTypes.bool,
})
),
/** Callback function to inform toolbarService of important events */
onInteraction: PropTypes.func.isRequired,
SplitButton.defaultProps = {
isToggle: false,
renderer: null,
isActive: false,
Component: null,
};
export default SplitButton;
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import StudyItem from '../StudyItem';
import ButtonGroup from '../ButtonGroup';
import LegacyButtonGroup from '../LegacyButtonGroup';
import LegacyButton from '../LegacyButton';
import ThumbnailList from '../ThumbnailList';
import { StringNumber } from '../../types';
@@ -74,8 +74,8 @@ const StudyBrowser = ({
className="w-100 border-secondary-light bg-primary-dark flex h-16 flex-row items-center justify-center border-b p-4"
data-cy={'studyBrowser-panel'}
>
{/* TODO Revisit design of ButtonGroup later - for now use LegacyButton for its children.*/}
<ButtonGroup
{/* TODO Revisit design of LegacyButtonGroup later - for now use LegacyButton for its children.*/}
<LegacyButtonGroup
variant="outlined"
color="secondary"
splitBorder={false}
@@ -108,7 +108,7 @@ const StudyBrowser = ({
</LegacyButton>
);
})}
</ButtonGroup>
</LegacyButtonGroup>
</div>
<div className="ohif-scrollbar invisible-scrollbar flex flex-1 flex-col overflow-auto">
{getTabContent()}
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import LegacyButton from '../LegacyButton';
import ButtonGroup from '../ButtonGroup';
import LegacyButtonGroup from '../LegacyButtonGroup';
import Typography from '../Typography';
import Select from '../Select';
@@ -50,8 +50,8 @@ const StudyListPagination = ({ onChangePage, currentPage, perPage, onChangePerPa
<Typography className="mr-4 text-base opacity-60">
{t('Page')} {currentPage}
</Typography>
{/* TODO Revisit design of ButtonGroup later - for now use LegacyButton for its children.*/}
<ButtonGroup>
{/* TODO Revisit design of LegacyButtonGroup later - for now use LegacyButton for its children.*/}
<LegacyButtonGroup>
<LegacyButton
size="initial"
className="px-4 py-2 text-base"
@@ -82,7 +82,7 @@ const StudyListPagination = ({ onChangePage, currentPage, perPage, onChangePerPa
>
{t('Next')}
</LegacyButton>
</ButtonGroup>
</LegacyButtonGroup>
</div>
</div>
</div>
@@ -15,14 +15,11 @@ const ToolbarButton = ({
onInteraction,
dropdownContent,
//
isActive: _isActive,
isActive,
className,
bState = {},
...rest
//
}) => {
const { primaryToolId } = bState;
const isActive = _isActive || (type === 'tool' && id === primaryToolId);
const classes = {
tool: isActive
? 'text-black'
@@ -85,6 +82,13 @@ ToolbarButton.propTypes = {
type: PropTypes.oneOf(['action', 'toggle', 'tool']),
id: PropTypes.string.isRequired,
isActive: PropTypes.bool,
className: PropTypes.string,
commands: PropTypes.arrayOf(
PropTypes.shape({
commandName: PropTypes.string.isRequired,
commandOptions: PropTypes.object,
})
),
onInteraction: PropTypes.func.isRequired,
icon: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
+8
View File
@@ -74,9 +74,14 @@ import LoadingIndicatorProgress from './LoadingIndicatorProgress';
import LoadingIndicatorTotalPercent from './LoadingIndicatorTotalPercent';
import ViewportActionBar from './ViewportActionBar';
import ProgressLoadingBar from './ProgressLoadingBar';
import PanelSection from './PanelSection';
import AdvancedToolbox from './AdvancedToolbox';
import InputDoubleRange from './InputDoubleRange';
import LegacyButtonGroup from './LegacyButtonGroup';
export {
AboutModal,
AdvancedToolbox,
HotkeyField,
Header,
UserPreferences,
@@ -101,6 +106,7 @@ export {
InputFilterText,
InputGroup,
InputRange,
InputDoubleRange,
InputNumber,
InputLabelWrapper,
InputMultiSelect,
@@ -109,6 +115,7 @@ export {
Label,
LayoutSelector,
LegacyButton,
LegacyButtonGroup,
LegacyCinePlayer,
LegacyViewportActionBar,
LoadingIndicatorProgress,
@@ -118,6 +125,7 @@ export {
NavBar,
Notification,
ProgressLoadingBar,
PanelSection,
Select,
SegmentationTable,
SegmentationGroupTable,
@@ -70,7 +70,9 @@ const determineActiveViewportId = (state: DefaultState, newViewports: Map) => {
const currentActiveViewport = state.viewports.get(activeViewportId);
if (!currentActiveViewport) {
return null; // Return an appropriate value if the current active viewport is not found
// if there is no active viewport, we should just return the first viewport
const firstViewport = newViewports.values().next().value;
return firstViewport.viewportOptions.viewportId;
}
// for the new viewports, we should rank them by the displaySetInstanceUIDs
@@ -135,6 +137,10 @@ export function ViewportGridProvider({ children, service }) {
payload.forEach(updatedViewport => {
const { viewportId, displaySetInstanceUIDs } = updatedViewport;
if (!viewportId) {
throw new Error('ViewportId is required to set display sets for viewport');
}
const previousViewport = viewports.get(viewportId);
// Use the newly provide viewportOptions and display set options
@@ -142,8 +148,8 @@ export function ViewportGridProvider({ children, service }) {
// That allows for easy updates of just the display set.
const viewportOptions = merge(
{},
updatedViewport?.viewportOptions,
previousViewport?.viewportOptions
previousViewport?.viewportOptions,
updatedViewport?.viewportOptions
);
const displaySetOptions = updatedViewport.displaySetOptions || [];
@@ -257,7 +263,7 @@ export function ViewportGridProvider({ children, service }) {
}
activeViewportIdToSet =
activeViewportIdToSet ?? determineActiveViewportId(state, viewports) ?? 'default';
activeViewportIdToSet ?? determineActiveViewportId(state, viewports);
const ret = {
...state,
+4
View File
@@ -36,6 +36,7 @@ export {
/** COMPONENTS */
export {
AboutModal,
AdvancedToolbox,
HotkeyField,
Header,
UserPreferences,
@@ -57,6 +58,7 @@ export {
IconButton,
Input,
InputRange,
InputDoubleRange,
InputNumber,
InputDateRange,
InputFilterText,
@@ -67,6 +69,7 @@ export {
Label,
LayoutSelector,
LegacyButton,
LegacyButtonGroup,
LegacyCinePlayer,
LegacyViewportActionBar,
LoadingIndicatorProgress,
@@ -75,6 +78,7 @@ export {
Modal,
NavBar,
Notification,
PanelSection,
ProgressLoadingBar,
Select,
SegmentationTable,
+6
View File
@@ -5,6 +5,12 @@
@tailwind components;
@tailwind utilities;
@layer base {
html {
font-family: 'Inter', sans-serif;
}
}
body {
background-color: #000;
}
+13
View File
@@ -0,0 +1,13 @@
/**
* Calculates the maximum number of digits required to display a value based on the maximum value and step size.
* @param maxValue - The maximum value to display.
* @param step - The step size between values.
* @returns The maximum number of digits required to display a value.
*/
const getMaxDigits = (maxValue: number, step: number) => {
const integerDigits = maxValue.toString().length;
const decimalDigits = step % 1 === 0 ? 0 : step.toString().split('.')[1].length;
return integerDigits + (decimalDigits ? decimalDigits + 1 : 0);
};
export default getMaxDigits;