feat(ViewportActionMenu): window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering (#3963)

Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
This commit is contained in:
IbrahimandJoe Boccanfuso authored and GitHub committed 2024-04-02 09:35:30 -04:00
1 parent 07301fc020
commit b7f90e3951
164 files changed
+5887 -932

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="19px" viewBox="0 0 18 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>action-new-dialog</title>
<g id="*****Volume-Render-Dialog" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="volume-rendering-dialog" transform="translate(-696, -178)">
<g id="action-new-dialog" transform="translate(696, 178.5)">
<rect id="Rectangle" x="0" y="0" width="18" height="18"></rect>
<path d="M6.73167982,5 L6.73167982,6 L3.23167982,6 C2.40325269,6 1.73167982,6.67157288 1.73167982,7.5 L1.73167982,14.5 C1.73167982,15.3284271 2.40325269,16 3.23167982,16 L10.2316798,16 C11.0601069,16 11.7316798,15.3284271 11.7316798,14.5 L11.7316798,11.5 L12.7316798,11.5 L12.7316798,14.5 C12.7316798,15.8807119 11.6123917,17 10.2316798,17 L3.23167982,17 C1.85096795,17 0.73167982,15.8807119 0.73167982,14.5 L0.73167982,7.5 C0.73167982,6.11928813 1.85096795,5 3.23167982,5 L6.73167982,5 Z" id="Path" fill="currentColor" fill-rule="nonzero"></path>
<line x1="8.23167982" y1="9.5" x2="16.2316798" y2="1.5" id="Path" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></line>
<polyline id="Path" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" points="11.2316798 1.5 16.2316798 1.5 16.2316798 6.5"></polyline>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="6px" height="10px" viewBox="0 0 6 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>chevron-next</title>
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard" transform="translate(-460, -343)" fill="#FFFFFF">
<polygon id="chevron-next" transform="translate(463, 348) scale(-1, 1) rotate(90) translate(-463, -348)" points="463 351 458 345.736842 458.7 345 463 349.526316 467.3 345 468 345.736842"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 609 B

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="6px" height="10px" viewBox="0 0 6 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>chevron-prev</title>
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard" transform="translate(-435, -343)" fill="#FFFFFF">
<polygon id="chevron-prev" transform="translate(438, 348) rotate(90) translate(-438, -348)" points="438 351 433 345.736842 433.7 345 438 349.526316 442.3 345 443 345.736842"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 596 B

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="29px" viewBox="0 0 28 29" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-chevron-patient</title>
<g id="Production" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard" transform="translate(-154, -234)">
<g id="icon-chevron-patient" transform="translate(168, 248.5) scale(-1, -1) translate(-168, -248.5)translate(154, 234.5)">
<rect id="Rectangle" x="0" y="0" width="28" height="28"></rect>
<polyline id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(14.5, 14) scale(-1, 1) translate(-14.5, -14)" points="16.5 10 12.5 14 16.5 18"></polyline>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 867 B

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-color-lut</title>
<g id="Production" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard" transform="translate(-58, -175)">
<g id="icon-color-lut" transform="translate(58, 175)">
<rect id="Rectangle" x="0" y="0" width="28" height="28"></rect>
<g id="Group-3" transform="translate(7, 6)">
<polygon id="Path-4" fill="#3378C8" points="5.3653608 15.2885085 12.1735461 7.20931345 13.2130397 11.248911 11.2982009 14.5315772 7.60883934 15.7670603"></polygon>
<polygon id="Path-3" fill="#21DC55" points="2.90861351 14.1910201 10.8161464 4.93194681 12.0148357 7.26808935 5.07756073 15.5719157"></polygon>
<polygon id="Path-2" fill="#E6FF00" points="1.11114723 11.6564599 8.96449984 2.52781588 9.14436752 2.35441261 10.65568 4.86937719 2.6199892 14.3240977"></polygon>
<path d="M0.654581374,10.5573035 L1.07159836,7.1163035 L3.45887024,3.82405096 L6.90861351,0 L8.67637665,2.14325052 C3.73187398,8.46470975 1.14061179,11.7502697 0.902590102,11.9999304 C0.664568411,12.2495912 0.581898835,11.7687155 0.654581374,10.5573035 Z" id="Path-5" fill="#E60000"></path>
<g id="Group" transform="translate(6.6939, 9.3583) rotate(40) translate(-6.6939, -9.3583)translate(3.4103, 2.8577)" stroke="#041C4A" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25">
<line x1="0.5" y1="11.5" x2="0.5" y2="2.19671376e-13" id="Path"></line>
<line x1="3.420153" y1="0.250185845" x2="3.420153" y2="13.0011428" id="Path"></line>
<line x1="6.06728889" y1="0.867657175" x2="6.06728889" y2="11.7034832" id="Path"></line>
</g>
<path d="M7.52683573,0.255234787 C7.36570938,0.0922486197 7.14226748,0 6.90861351,0 C6.67495954,0 6.45151764,0.0922486197 6.29039129,0.255234787 C5.12761351,1.43158261 0.408613509,6.42775653 0.408613509,10.086887 C0.408613509,13.5449739 3.31844684,15.9999304 6.90861351,15.9999304 C10.4987802,15.9999304 13.4086135,13.5449739 13.4086135,10.086887 C13.4086135,6.42775653 8.68961351,1.43158261 7.52683573,0.255234787 Z" id="Path" stroke="#E1E1E1" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-multiple-patients</title>
<g id="4D-assets---final" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard" transform="translate(-171, -280)">
<g id="icon-multiple-patients" transform="translate(171, 280)">
<rect id="Rectangle" x="0" y="0" width="28" height="28"></rect>
<g id="Group-3" transform="translate(2, 1.5)" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<g id="patient-icon" transform="translate(9, 10)">
<circle id="Oval" cx="7.5" cy="7.5" r="7.5"></circle>
<path d="M2.331,12.935 C3.39163827,12.4490078 4.47638164,12.0174468 5.581,11.642 C6.189,11.416 6.09,9.827 5.82,9.531 C4.95198021,8.59033128 4.52668888,7.3239163 4.651,6.05 C4.5727272,5.23902161 4.83519788,4.43201369 5.37555564,3.82223826 C5.9159134,3.21246283 6.68549454,2.85483523 7.5,2.835 C8.31450546,2.85483523 9.0840866,3.21246283 9.62444436,3.82223826 C10.1648021,4.43201369 10.4272728,5.23902161 10.349,6.05 C10.4733111,7.3239163 10.0480198,8.59033128 9.18,9.531 C8.91,9.831 8.811,11.416 9.419,11.642 C10.5236184,12.0174468 11.6083617,12.4490078 12.669,12.935" id="Path"></path>
</g>
<g id="patient-icon">
<path d="M7.5,0 C11.6421356,0 15,3.35786438 15,7.5 C15,8.48189915 14.8113103,9.41972745 14.4682129,10.2792028 C12.213795,10.9117859 10.3898771,12.5712406 9.53178711,14.7207972 C8.88630634,14.9028658 8.20449663,15 7.5,15 C3.35786438,15 0,11.6421356 0,7.5 C0,3.35786438 3.35786438,0 7.5,0 Z" id="Combined-Shape"></path>
<path d="M2.331,12.935 C3.39163827,12.4490078 4.47638164,12.0174468 5.581,11.642 C6.189,11.416 6.09,9.827 5.82,9.531 C4.95198021,8.59033128 4.52668888,7.3239163 4.651,6.05 C4.5727272,5.23902161 4.83519788,4.43201369 5.37555564,3.82223826 C5.9159134,3.21246283 6.68549454,2.85483523 7.5,2.835 C8.31450546,2.85483523 9.0840866,3.21246283 9.62444436,3.82223826 C10.1648021,4.43201369 10.4272728,5.23902161 10.349,6.05 C10.4733111,7.3239163 10.0480198,8.59033128 9.18,9.531 C8.91,9.831 8.811,11.416 9.419,11.642 C10.5236184,12.0174468 10.5236184,12.0174468 10.9701462,12.2764988" id="Path"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-patient</title>
<g id="Production" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard" transform="translate(-104, -235)">
<g id="icon-patient" transform="translate(104, 235)">
<rect id="Rectangle" x="0" y="0" width="28" height="28"></rect>
<circle id="Oval" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" cx="14" cy="14" r="9"></circle>
<path d="M7.7972,20.522 C9.06996592,19.9388093 10.371658,19.4209361 11.6972,18.9704 C12.4268,18.6992 12.308,16.7924 11.984,16.4372 C10.9423763,15.3083975 10.4320267,13.7886996 10.5812,12.26 C10.4872726,11.2868259 10.8022375,10.3184164 11.4506668,9.58668591 C12.0990961,8.8549554 13.0225934,8.42580227 14,8.402 C14.9774066,8.42580227 15.9009039,8.8549554 16.5493332,9.58668591 C17.1977625,10.3184164 17.5127274,11.2868259 17.4188,12.26 C17.5679733,13.7886996 17.0576237,15.3083975 16.016,16.4372 C15.692,16.7972 15.5732,18.6992 16.3028,18.9704 C17.628342,19.4209361 18.9300341,19.9388093 20.2028,20.522" id="Path" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-settings</title>
<g id="Production" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard" transform="translate(-211, -235)">
<g id="icon-settings" transform="translate(211, 235)">
<rect id="Rectangle" x="0" y="0" width="28" height="28"></rect>
<path d="M20.2015688,10.2525279 C20.0232123,10.6274217 20.0120959,11.0603422 20.1709774,11.4438954 C20.3298588,11.8274487 20.6438515,12.1256974 21.0350638,12.2646554 L22.0250838,12.6168364 C22.6105106,12.8246232 23.00167,13.3785255 23.00167,13.9997339 C23.00167,14.6209423 22.6105106,15.1748446 22.0250838,15.3826314 L21.0350638,15.7348124 C20.6438515,15.8737704 20.3298588,16.1720191 20.1709774,16.5555724 C20.0120959,16.9391256 20.0232123,17.3720461 20.2015688,17.7469399 L20.6539257,18.6946982 C20.9219287,19.2558525 20.8071211,19.9250005 20.367394,20.3647276 C19.9276669,20.8044547 19.2585189,20.9192624 18.6973645,20.6512594 L17.748041,20.2004677 C17.3731472,20.0221111 16.9402267,20.0109948 16.5566735,20.1698763 C16.1731202,20.3287577 15.8748715,20.6427504 15.7359135,21.0339627 L15.3837325,22.0239827 C15.1756233,22.6088399 14.6220059,22.9994678 14.0012263,22.9994678 C13.3804467,22.9994678 12.8268293,22.6088399 12.6187202,22.0239827 L12.2665391,21.0339627 C12.127404,20.6426994 11.8290064,20.3287067 11.4453321,20.1698369 C11.0616578,20.0109671 10.6286351,20.0220972 10.253629,20.2004677 L9.30587073,20.6512594 C8.7446872,20.9203194 8.07479926,20.8059063 7.63473092,20.365838 C7.19466259,19.9257696 7.08024945,19.2558817 7.34930952,18.6946982 L7.80010123,17.7453747 C7.97845774,17.3704809 7.98957409,16.9375604 7.83069263,16.5540071 C7.67181118,16.1704539 7.35781846,15.8722052 6.96660615,15.7332471 L5.97658618,15.3810661 C5.39115942,15.1732793 5,14.619377 5,13.9981686 C5,13.3769603 5.39115942,12.8230579 5.97658618,12.6152712 L6.96660615,12.2630902 C7.35740035,12.124078 7.67105878,11.8260915 7.82990186,11.4429292 C7.98874494,11.0597669 7.97791757,10.6272622 7.80010123,10.2525279 L7.34930952,9.30320437 C7.08024945,8.74202085 7.19466259,8.0721329 7.63473092,7.63206456 C8.07479926,7.19199623 8.7446872,7.07758309 9.30587073,7.34664317 L10.2551942,7.79743487 C10.6298363,7.97533367 11.0622628,7.98639209 11.445508,7.82787471 C11.8287532,7.66935733 12.1270239,7.35606892 12.2665391,6.96550504 L12.6187202,5.97548507 C12.8268293,5.39062793 13.3804467,5 14.0012263,5 C14.6220059,5 15.1756233,5.39062793 15.3837325,5.97548507 L15.7359135,6.96550504 C15.8748715,7.35671735 16.1731202,7.67071008 16.5566735,7.82959153 C16.9402267,7.98847298 17.3731472,7.97735664 17.748041,7.79900012 L18.6973645,7.34664317 C19.2585189,7.07864018 19.9276669,7.19344783 20.367394,7.63317492 C20.8071211,8.07290202 20.9219287,8.74204999 20.6539257,9.30320437 L20.2015688,10.2525279 Z" id="Path" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"></path>
<circle id="Oval" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" cx="14.000835" cy="13.9997339" r="3.52181017"></circle>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="29px" viewBox="0 0 28 29" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-toolbar-back</title>
<g id="Production" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard" transform="translate(-56, -234)">
<g id="icon-toolbar-back" transform="translate(56, 234.5)">
<rect id="Rectangle" x="0" y="0" width="28" height="28"></rect>
<path d="M15.7247114,7.29289322 C16.1152357,6.90236893 16.7484007,6.90236893 17.138925,7.29289322 C17.4994089,7.65337718 17.5271385,8.22060824 17.2221136,8.61289944 L17.138925,8.70710678 L11.9132727,13.9312727 L17.138925,19.1565296 C17.4994089,19.5170135 17.5271385,20.0842446 17.2221136,20.4765358 L17.138925,20.5707431 C16.778441,20.9312271 16.2112099,20.9589566 15.8189187,20.6539318 L15.7247114,20.5707431 L9.79289322,14.638925 C9.43240926,14.278441 9.40467972,13.7112099 9.70970461,13.3189187 L9.79289322,13.2247114 L15.7247114,7.29289322 Z" id="back" fill="currentColor"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>arrow-right</title>
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard" transform="translate(-310, -87)">
<g id="arrow-right" transform="translate(310, 87)">
<rect id="Rectangle" opacity="0.5" x="0" y="0" width="24" height="24"></rect>
<polygon id="Path" fill="currentColor" fill-rule="nonzero" points="10.41 6 9 7.41 13.58 12 9 16.59 10.41 18 16.41 12"></polygon>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 704 B

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>arrow-left</title>
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard" transform="translate(-268, -87)">
<g id="arrow-left" transform="translate(280, 99) scale(-1, 1) translate(-280, -99)translate(268, 87)">
<rect id="Rectangle" opacity="0.5" x="0" y="0" width="24" height="24"></rect>
<polygon id="Path" fill="currentColor" fill-rule="nonzero" points="10.41 6 9 7.41 13.58 12 9 16.59 10.41 18 16.41 12"></polygon>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 754 B

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>viewport-status-tracfked</title>
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard" transform="translate(-142, -87)">
<g id="viewport-status-tracfked" transform="translate(142, 87)">
<rect id="Rectangle" x="0" y="0" width="24" height="24"></rect>
<rect id="Rectangle" stroke="#5ACCE6" fill="#5ACCE6" x="4.5" y="4.5" width="15" height="15" rx="7.5"></rect>
<path d="M15.388889,9 L11.7739644,14.5948033 C11.6112717,14.8456871 11.3630166,15.0025668 11.0931982,15.0249993 C10.8233798,15.0474318 10.5584004,14.9332222 10.3665704,14.7118131 L8.5,12.5449644" id="Path" stroke="#090C29" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1007 B

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>viewport-window-level</title>
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard" transform="translate(-184, -87)">
<g id="viewport-window-level" transform="translate(184, 87)">
<rect id="Rectangle" opacity="0.5" x="0" y="0" width="24" height="24"></rect>
<g id="Group" transform="translate(4, 4)" fill="currentColor">
<g id="icon-info-link">
<path d="M8,0 C3.581722,0 0,3.581722 0,8 C0,12.418278 3.581722,16 8,16 C12.418278,16 16,12.418278 16,8 C16,3.581722 12.418278,0 8,0 Z M8,1.01463415 C11.857911,1.01463415 14.9853659,4.14208897 14.9853659,8 C14.9853659,11.857911 11.857911,14.9853659 8,14.9853659 C4.14208897,14.9853659 1.01463415,11.857911 1.01463415,8 C1.01463415,4.14208897 4.14208897,1.01463415 8,1.01463415 Z" id="Oval"></path>
</g>
<path d="M13.9802015,2.99802095 C15.2351212,4.37868604 16,6.21291722 16,8.2258274 C16,12.5193844 12.5200077,16 8.22721968,16 C6.21466999,16 4.38076731,15.2349842 3.00034948,13.9798397 C3.38388248,13.5964084 3.81184081,13.1683734 4.28334632,12.6967835 L4.86417397,12.1158518 C5.13143061,11.8485473 5.41083528,11.5690926 5.70238798,11.2774876 L7.59748054,9.38205562 C8.61791499,8.36143838 9.74768171,7.2314693 10.9867807,5.99214837 Z" id="Combined-Shape"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+8 -6
View File
@@ -1,7 +1,9 @@
/* CUSTOM OHIF SCROLLBAR */
.ohif-scrollbar {
scrollbar-color: #173239 transparent;
scrollbar-color: #041c4a transparent;
scrollbar-gutter: stable;
scrollbar-width: thin;
}
.study-min-height {
@@ -13,7 +15,7 @@
}
.ohif-scrollbar::-webkit-scrollbar {
width: 8px;
scrollbar-width: thin;
}
.ohif-scrollbar::-webkit-scrollbar-track {
@@ -22,13 +24,13 @@
.ohif-scrollbar::-webkit-scrollbar-thumb {
@apply rounded;
@apply bg-primary-main;
background-color: #173239;
@apply bg-secondary-dark;
background-color: #041c4a;
}
.ohif-scrollbar::-webkit-scrollbar-thumb:window-inactive {
@apply bg-primary-main;
background-color: #173239;
@apply bg-secondary-dark;
background-color: #041c4a;
}
/* INVISIBLE SCROLLBAR */
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="138px" height="28px" viewBox="0 0 138 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>toolbar-branding-open-health-imaging-foundation</title>
<g id="Production" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard" transform="translate(-74, -297)">
<g id="toolbar-branding-open-health-imaging-foundation" transform="translate(74, 297)">
<rect id="Rectangle" x="0" y="0" width="138" height="28"></rect>
<path d="M37.7225261,11.2237762 C39.7660251,11.2237762 41.2479518,9.64468611 41.2479518,7.11188811 C41.2479518,4.57909011 39.7660251,3 37.7225261,3 C35.6790271,3 34.1971004,4.57909011 34.1971004,7.11188811 C34.1971004,9.64468611 35.6790271,11.2237762 37.7225261,11.2237762 Z M37.8064648,10.3846154 C36.3486619,10.3846154 35.2043649,9.26464267 35.2043649,7.1958042 C35.2043649,5.12696572 36.3486619,4.00699301 37.8064648,4.00699301 C39.2642678,4.00699301 40.4085648,5.12696572 40.4085648,7.1958042 C40.4085648,9.26464267 39.2642678,10.3846154 37.8064648,10.3846154 Z M43.6645943,13.2377622 L43.6645943,10.1056298 L43.7413524,10.1056298 C43.9409235,10.4296435 44.324714,11.1393878 45.4607341,11.1393878 C46.9344898,11.1393878 47.9630485,9.95133759 47.9630485,8.0689723 C47.9630485,6.20203624 46.9344898,5.01398601 45.4453824,5.01398601 C44.2940108,5.01398601 43.9409235,5.7237303 43.7413524,6.03231478 L43.633891,6.03231478 L43.633891,5.09113213 L42.7588486,5.09113213 L42.7588486,13.2377622 L43.6645943,13.2377622 Z M45.4297631,10.3846154 C44.3559526,10.3846154 43.7661131,9.45029198 43.7661131,8.09552303 C43.7661131,6.75632615 44.3408286,5.85314685 45.4297631,5.85314685 C46.56407,5.85314685 47.1236614,6.83418643 47.1236614,8.09552303 C47.1236614,9.37243169 46.5489459,10.3846154 45.4297631,10.3846154 Z M51.8942607,11.2237762 C53.0952299,11.2237762 53.9728613,10.6137465 54.250008,9.7065227 L53.3723767,9.45625407 C53.141421,10.0819256 52.6063739,10.3947614 51.8942607,10.3947614 C50.8280156,10.3947614 50.0928069,9.69479135 50.0504651,8.40825421 L54.3423903,8.40825421 L54.3423903,8.01720949 C54.3423903,5.78043367 53.0336418,5.01398601 51.8018785,5.01398601 C50.2005862,5.01398601 49.1381904,6.29661271 49.1381904,8.1423438 C49.1381904,9.9880749 50.1851892,11.2237762 51.8942607,11.2237762 Z M53.5030032,7.53146853 L50.1454549,7.53146853 C50.20685,6.64403738 50.8553365,5.85314685 51.89138,5.85314685 C52.8737027,5.85314685 53.5030032,6.56606226 53.5030032,7.53146853 Z M56.7655682,11.0559441 L56.7655682,7.46804609 C56.7655682,6.42740036 57.4072575,5.83718338 58.2808828,5.83718338 C59.1274488,5.83718338 59.6415734,6.39245331 59.6415734,7.32825786 L59.6415734,11.0559441 L60.5538546,11.0559441 L60.5538546,7.26612976 C60.5538546,5.74399123 59.7459446,5.01398601 58.5437435,5.01398601 C57.6469246,5.01398601 57.0902785,5.41781869 56.8119554,6.02356769 L56.7346434,6.02356769 L56.7346434,5.09164614 L55.853287,5.09164614 L55.853287,11.0559441 L56.7655682,11.0559441 Z M66.2244746,11.0559441 L66.2244746,7.52786276 L70.4958343,7.52786276 L70.4958343,11.0559441 L71.4658866,11.0559441 L71.4658866,3.16783217 L70.4958343,3.16783217 L70.4958343,6.68050699 L66.2244746,6.68050699 L66.2244746,3.16783217 L65.2544223,3.16783217 L65.2544223,11.0559441 L66.2244746,11.0559441 Z M75.7328537,11.2237762 C76.9338229,11.2237762 77.8114543,10.6137465 78.088601,9.7065227 L77.2109697,9.45625407 C76.980014,10.0819256 76.4449669,10.3947614 75.7328537,10.3947614 C74.6666086,10.3947614 73.9313999,9.69479135 73.889058,8.40825421 L78.1809832,8.40825421 L78.1809832,8.01720949 C78.1809832,5.78043367 76.8722347,5.01398601 75.6404715,5.01398601 C74.0391792,5.01398601 72.9767834,6.29661271 72.9767834,8.1423438 C72.9767834,9.9880749 74.0237822,11.2237762 75.7328537,11.2237762 Z M77.3415962,7.53146853 L73.9840479,7.53146853 C74.045443,6.64403738 74.6939295,5.85314685 75.729973,5.85314685 C76.7122957,5.85314685 77.3415962,6.56606226 77.3415962,7.53146853 Z M81.3619048,11.2237762 C82.3877616,11.2237762 82.9236569,10.6620867 83.1073925,10.2720245 L83.1533264,10.2720245 L83.1533264,11.0833538 L84.0566928,11.0833538 L84.0566928,7.13592438 C84.0566928,5.23242085 82.6327423,5.01398601 81.8824889,5.01398601 C80.9944337,5.01398601 79.9838882,5.32603577 79.5245494,6.41820993 L80.3819819,6.73025969 C80.5810288,6.29339003 81.0518511,5.82531539 81.9131114,5.82531539 C82.7437492,5.82531539 83.1533264,6.27388692 83.1533264,7.04230945 L83.1533264,7.07351443 C83.1533264,7.51818533 82.7092988,7.47917911 81.6375081,7.6196015 C80.5465783,7.76392452 79.3561251,8.0096637 79.3561251,9.38268264 C79.3561251,10.5528692 80.2441803,11.2237762 81.3619048,11.2237762 Z M81.5864965,10.3846154 C80.8767925,10.3846154 80.3633896,10.0534566 80.3633896,9.40690853 C80.3633896,8.69728258 80.9824931,8.47651007 81.677097,8.38189327 C82.0545992,8.33458488 83.0663049,8.22419862 83.2173057,8.03496503 L83.2173057,8.88651617 C83.2173057,9.64345051 82.6435025,10.3846154 81.5864965,10.3846154 Z M86.7427314,11.0559441 L86.7427314,3.16783217 L85.9033443,3.16783217 L85.9033443,11.0559441 L86.7427314,11.0559441 Z M90.4262553,11.2237762 C90.7668998,11.2237762 90.9836735,11.1623657 91.1075442,11.1163078 L90.9217382,10.3026184 C90.844319,10.317971 90.7204482,10.3486763 90.5191583,10.3486763 C90.1165785,10.3486763 89.7294825,10.2258552 89.7294825,9.45822369 L89.7294825,6.0192344 L90.9991573,6.0192344 L90.9991573,5.25160287 L89.7294825,5.25160287 L89.7294825,3.83916084 L88.815936,3.83916084 L88.815936,5.25160287 L87.9178Line truncated
<g id="Group" transform="translate(3, 3.0579)" fill="#FFFFFF" fill-rule="nonzero" stroke="#FFFFFF" stroke-width="0.25">
<path d="M20.874737,0 L14.2607623,0 C13.3583609,0 12.6268201,0.731540857 12.6268201,1.63394224 L12.6268201,8.24791696 C12.6268201,9.15031834 13.3583609,9.88185919 14.2607623,9.88185919 L20.874737,9.88185919 C21.7771384,9.88185919 22.5086793,9.15031834 22.5086793,8.24791696 L22.5086793,1.63394224 C22.5086793,0.731540857 21.7771384,0 20.874737,0 Z M14.2607623,0.653576894 L20.874737,0.653576894 C21.4161779,0.653576894 21.8551024,1.09250141 21.8551024,1.63394224 L21.8551024,8.24791696 C21.8551024,8.78935779 21.4161779,9.2282823 20.874737,9.2282823 L14.2607623,9.2282823 C13.7193215,9.2282823 13.280397,8.78935779 13.280397,8.24791696 L13.280397,1.63394224 C13.280397,1.09250141 13.7193215,0.653576894 14.2607623,0.653576894 Z" id="Rectangle"></path>
<path d="M8.24791696,0 L1.63394224,0 C0.731540857,0 0,0.731540857 0,1.63394224 L0,8.24791696 C0,9.15031834 0.731540857,9.88185919 1.63394224,9.88185919 L8.24791696,9.88185919 C9.15031834,9.88185919 9.88185919,9.15031834 9.88185919,8.24791696 L9.88185919,1.63394224 C9.88185919,0.731540857 9.15031834,0 8.24791696,0 Z M1.63394224,0.653576894 L8.24791696,0.653576894 C8.78935779,0.653576894 9.2282823,1.09250141 9.2282823,1.63394224 L9.2282823,8.24791696 C9.2282823,8.78935779 8.78935779,9.2282823 8.24791696,9.2282823 L1.63394224,9.2282823 C1.09250141,9.2282823 0.653576894,8.78935779 0.653576894,8.24791696 L0.653576894,1.63394224 C0.653576894,1.09250141 1.09250141,0.653576894 1.63394224,0.653576894 Z" id="Rectangle"></path>
<path d="M20.874737,12.6268201 L14.2607623,12.6268201 C13.3583609,12.6268201 12.6268201,13.3583609 12.6268201,14.2607623 L12.6268201,20.874737 C12.6268201,21.7771384 13.3583609,22.5086793 14.2607623,22.5086793 L20.874737,22.5086793 C21.7771384,22.5086793 22.5086793,21.7771384 22.5086793,20.874737 L22.5086793,14.2607623 C22.5086793,13.3583609 21.7771384,12.6268201 20.874737,12.6268201 Z M14.2607623,13.280397 L20.874737,13.280397 C21.4161779,13.280397 21.8551024,13.7193215 21.8551024,14.2607623 L21.8551024,20.874737 C21.8551024,21.4161779 21.4161779,21.8551024 20.874737,21.8551024 L14.2607623,21.8551024 C13.7193215,21.8551024 13.280397,21.4161779 13.280397,20.874737 L13.280397,14.2607623 C13.280397,13.7193215 13.7193215,13.280397 14.2607623,13.280397 Z" id="Rectangle"></path>
<path d="M8.24791696,12.6268201 L1.63394224,12.6268201 C0.731540857,12.6268201 0,13.3583609 0,14.2607623 L0,20.874737 C0,21.7771384 0.731540857,22.5086793 1.63394224,22.5086793 L8.24791696,22.5086793 C9.15031834,22.5086793 9.88185919,21.7771384 9.88185919,20.874737 L9.88185919,14.2607623 C9.88185919,13.3583609 9.15031834,12.6268201 8.24791696,12.6268201 Z M1.63394224,13.280397 L8.24791696,13.280397 C8.78935779,13.280397 9.2282823,13.7193215 9.2282823,14.2607623 L9.2282823,20.874737 C9.2282823,21.4161779 8.78935779,21.8551024 8.24791696,21.8551024 L1.63394224,21.8551024 C1.09250141,21.8551024 0.653576894,21.4161779 0.653576894,20.874737 L0.653576894,14.2607623 C0.653576894,13.7193215 1.09250141,13.280397 1.63394224,13.280397 Z" id="Rectangle"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,27 @@
import React from 'react';
import { Icon } from '@ohif/ui';
import DividerItem from './DividerItem';
type BackItemProps = {
backLabel?: string;
onBackClick: () => void;
};
const BackItem = ({ backLabel, onBackClick }: BackItemProps) => {
return (
<>
<div
className="all-in-one-menu-item all-in-one-menu-item-effects"
onClick={onBackClick}
>
<Icon name="content-prev"></Icon>
<div className="pl-2">{backLabel || 'Back to Display Options'}</div>
</div>
<DividerItem></DividerItem>
</>
);
};
export default BackItem;
@@ -0,0 +1,11 @@
import React from 'react';
const DividerItem = () => {
return (
<div className="flex h-3.5 shrink-0 items-center px-2">
<div className="bg-primary-dark h-[2px] w-full"></div>
</div>
);
};
export default DividerItem;
@@ -0,0 +1,13 @@
import React, { ReactNode } from 'react';
type HeaderItemProps = {
children: ReactNode;
};
const HeaderItem = ({ children }: HeaderItemProps) => {
return (
<div className="text-aqua-pale mx-2 flex h-6 shrink-0 items-center text-[11px]">{children}</div>
);
};
export default HeaderItem;
@@ -0,0 +1,82 @@
import React, { useCallback, useEffect, useState } from 'react';
import OutsideClickHandler from 'react-outside-click-handler';
import { MenuProps } from './Menu';
import getIcon from '../Icon/getIcon';
import classNames from 'classnames';
import { AllInOneMenu } from '..';
export interface IconMenuProps extends MenuProps {
icon: string;
iconClassName?: string;
horizontalDirection?: AllInOneMenu.HorizontalDirection;
verticalDirection?: AllInOneMenu.VerticalDirection;
menuKey?: number | string;
}
/**
* An IconMenu allows for a div wrapped icon to be clicked to show and hide
* an AllInOneMenu.Menu. Based on the direction(s) specified, the menu is
* positioned relative to the icon.
*
* HorizontalDirection.LeftToRight - the left edges of the icon and menu are aligned
* HorizontalDirection.RightRoLeft - the right edges of the icon and menu are aligned
* VerticalDirection.TopToBottom - the top edge of the menu appears directly below the bottom edge of the icon
* VerticalDirection.BottomToTop - the bottom edge of the menu appears directly above the top edge of the icon
*
* For example, if an IconMenu were situated in the bottom-left corner of a container,
* it would be best to use BottomToTop and LeftToRight directions for it.
*/
export default function IconMenu({
icon,
iconClassName,
horizontalDirection,
verticalDirection,
children,
backLabel,
menuClassName,
menuStyle,
onVisibilityChange,
menuKey,
}: IconMenuProps) {
const [isMenuVisible, setIsMenuVisible] = useState(false);
const toggleMenuVisibility = useCallback(() => setIsMenuVisible(isVisible => !isVisible), []);
return (
<OutsideClickHandler
onOutsideClick={toggleMenuVisibility}
disabled={!isMenuVisible}
>
<div className="relative">
<div
className={iconClassName}
onClick={toggleMenuVisibility}
>
{getIcon(icon)}
</div>
<AllInOneMenu.Menu
key={menuKey}
isVisible={isMenuVisible}
backLabel={backLabel}
menuClassName={classNames(
menuClassName,
'absolute',
verticalDirection === AllInOneMenu.VerticalDirection.TopToBottom
? 'top-[100%]'
: 'bottom-[100%]',
horizontalDirection === AllInOneMenu.HorizontalDirection.LeftToRight
? 'left-0'
: 'right-0'
)}
menuStyle={menuStyle}
onVisibilityChange={isVis => {
setIsMenuVisible(isVis);
onVisibilityChange?.(isVis);
}}
horizontalDirection={horizontalDirection}
>
{children}
</AllInOneMenu.Menu>
</div>
</OutsideClickHandler>
);
}
@@ -0,0 +1,45 @@
import React, { ReactNode, useCallback, useContext } from 'react';
import { MenuContext } from './Menu';
type ItemProps = {
label: string;
secondaryLabel?: string;
icon?: ReactNode;
onClick?: () => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
rightIcon?: ReactNode;
};
const Item = ({
label,
secondaryLabel,
icon,
rightIcon,
onClick,
onMouseEnter,
onMouseLeave,
}: ItemProps) => {
const { hideMenu } = useContext(MenuContext);
const onClickHandler = useCallback(() => {
hideMenu();
onClick?.();
}, [hideMenu, onClick]);
return (
<div
className="all-in-one-menu-item all-in-one-menu-item-effects"
onClick={onClickHandler}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{icon && <div className="pr-2">{icon}</div>}
<span>{label}</span>
{secondaryLabel != null && <span className="text-aqua-pale ml-[1ch]">{secondaryLabel}</span>}
{rightIcon && <div className="ml-auto">{rightIcon}</div>}
</div>
);
};
export default Item;
@@ -0,0 +1,29 @@
import React, { ReactNode, useContext, useEffect } from 'react';
import { MenuContext } from './Menu';
type ItemPanelProps = {
label?: string;
index?: number;
children: ReactNode;
};
const ItemPanel = ({ label, index = 0, children }: ItemPanelProps) => {
const { addItemPanel, activePanelIndex } = useContext(MenuContext);
useEffect(() => {
addItemPanel(index, label);
}, []);
return (
activePanelIndex === index && (
<div
style={{ scrollbarGutter: 'auto' }}
className="ohif-scrollbar flex flex-col overflow-auto"
>
{children}
</div>
)
);
};
export default ItemPanel;
@@ -0,0 +1,188 @@
import React, { createContext, ReactNode, useCallback, useEffect, useState } from 'react';
import './allInOneMenu.css';
import DividerItem from './DividerItem';
import PanelSelector from './PanelSelector';
import classNames from 'classnames';
import BackItem from './BackItem';
/**
* The vertical direction that the menu will be opened/used with.
*
* A TopToBottom menu would be used for cases where the menu is opened "near"
* the top edge of its container. Likewise a BottomToTop menu would be used
* for cases where the menu is opened "near" the bottom edge of its container.
*
* See IconMenu for more information.
*/
export enum VerticalDirection {
TopToBottom,
BottomToTop,
}
/**
* The horizontal direction that the menu is opened/used with.
* This direction dictates the general direction sub-menus and
* back-to-menus are opened with. For example, a RightToLeft menu
* will have sub-menu items indicated with a left pointing chevron
* and aligned with the left edge of the menu. Similarly back-to items of a
* RightToLeft menu are indicated with a right pointing chevron and
* aligned with the right edge of the menu.
*
* It is also worth noting that a LeftToRight menu would be used for
* cases where a menu is opened "near" the left edge of its container.
* Likewise, a RightToLeft menu would be used for cases where a menu is opened
* "near" the right edge of its container.
*
* See IconMenu for more information.
*/
export enum HorizontalDirection {
LeftToRight,
RightToLeft,
}
export interface MenuProps {
menuStyle?: unknown;
menuClassName?: string;
isVisible?: boolean;
preventHideMenu?: boolean;
backLabel?: string;
headerComponent?: ReactNode;
showHeaderDivider?: boolean;
activePanelIndex?: number;
onVisibilityChange?: (isVisible: boolean) => void;
horizontalDirection?: HorizontalDirection;
children: ReactNode;
}
type MenuContextProps = {
showSubMenu: (subMenuProps: MenuProps) => void;
hideMenu: () => void;
addItemPanel: (index: number, label: string) => void;
horizontalDirection: HorizontalDirection;
activePanelIndex: number;
};
type MenuPathState = {
props: MenuProps;
activePanelIndex: number;
};
export const MenuContext = createContext<MenuContextProps>(null);
const Menu = (props: MenuProps) => {
const {
isVisible,
onVisibilityChange,
activePanelIndex,
preventHideMenu,
menuClassName,
menuStyle,
horizontalDirection = HorizontalDirection.LeftToRight,
} = props;
const [isMenuVisible, setIsMenuVisible] = useState(isVisible);
// The menuPath is an array consisting of this top Menu and every SubMenu
// that has been traversed/opened by the user with the last item in the array
// being the current (sub)menu that is currently visible. This allows for the previously
// viewed menus to be returned to via the Back button at the top of the menu.
const [menuPath, setMenuPath] = useState<Array<MenuPathState>>([
{ props, activePanelIndex: activePanelIndex || 0 },
]);
const [itemPanelLabels, setItemPanelLabels] = useState<Array<string>>([]);
const hideMenu = useCallback(() => {
if (preventHideMenu) {
return;
}
setMenuPath(path => [path[0]]);
setItemPanelLabels([]);
setIsMenuVisible(false);
onVisibilityChange?.(false);
}, [preventHideMenu, onVisibilityChange]);
useEffect(() => {
if (isVisible) {
setIsMenuVisible(isVisible);
onVisibilityChange?.(isVisible);
} else {
hideMenu();
}
}, [hideMenu, isVisible, onVisibilityChange]);
const showSubMenu = useCallback((subMenuProps: MenuProps) => {
setMenuPath(path => {
return [
...path,
{ props: subMenuProps, activePanelIndex: subMenuProps.activePanelIndex || 0 },
];
});
setItemPanelLabels([]);
}, []);
const addItemPanel = useCallback((index, label) => {
setItemPanelLabels(labels => {
return [...labels.slice(0, index), label, ...labels.slice(index + 1, labels.length)];
});
}, []);
const onActivePanelIndexChange = useCallback(index => {
setMenuPath(path => {
return [
...path.slice(0, path.length - 1),
{ ...path[path.length - 1], activePanelIndex: index },
];
});
}, []);
const onBackClick = useCallback(() => {
setMenuPath(path => [...path.slice(0, path.length - 1)]);
setItemPanelLabels([]);
}, []);
const { props: currentMenuProps, activePanelIndex: currentMenuActivePanelIndex } =
menuPath[menuPath.length - 1];
return (
<>
<MenuContext.Provider
value={{
showSubMenu,
hideMenu,
addItemPanel,
activePanelIndex: currentMenuActivePanelIndex,
horizontalDirection,
}}
>
{isMenuVisible && (
<div
className={classNames(
'bg-secondary-dark flex select-none flex-col rounded px-1 py-1.5 text-white opacity-90',
menuClassName
)}
style={menuStyle}
>
{menuPath.length > 1 && (
<BackItem
backLabel={menuPath[menuPath.length - 2].props.backLabel}
onBackClick={onBackClick}
/>
)}
{itemPanelLabels.length > 1 && (
<PanelSelector
panelLabels={itemPanelLabels}
activeIndex={currentMenuActivePanelIndex}
onActiveIndexChange={onActivePanelIndexChange}
></PanelSelector>
)}
{currentMenuProps.headerComponent}
{currentMenuProps.showHeaderDivider && <DividerItem />}
{currentMenuProps.children}
</div>
)}
</MenuContext.Provider>
</>
);
};
export default Menu;
@@ -0,0 +1,31 @@
import React, { ReactNode } from 'react';
import { ButtonGroup } from '../../components';
type PanelSelectorProps = {
panelLabels: Array<ReactNode>;
onActiveIndexChange: (index: number) => void;
activeIndex: number;
};
const PanelSelector = ({ panelLabels, onActiveIndexChange, activeIndex }: PanelSelectorProps) => {
const getButtons = () => {
return panelLabels.map((panelLabel, index) => {
return {
children: panelLabel,
key: index,
};
});
};
return (
<div className="mx-2 my-1 flex justify-center">
<ButtonGroup
buttons={getButtons()}
onActiveIndexChange={onActiveIndexChange}
defaultActiveIndex={activeIndex}
></ButtonGroup>
</div>
);
};
export default PanelSelector;
@@ -0,0 +1,38 @@
import React, { useCallback, useContext } from 'react';
import { MenuContext, MenuProps } from './Menu';
import Icon from '../Icon';
export interface SubMenuProps extends MenuProps {
itemLabel: string;
onClick?: () => void;
itemIcon?: string;
}
const SubMenu = (props: SubMenuProps) => {
const { showSubMenu } = useContext(MenuContext);
const onClickHandler = useCallback(() => {
showSubMenu(props);
props.onClick?.();
}, [showSubMenu, props]);
return (
<div
className="all-in-one-menu-item all-in-one-menu-item-effects flex items-center"
onClick={onClickHandler}
>
{props.itemIcon && (
<Icon
name={props.itemIcon}
width="25px"
height="25px"
className="mr-2"
></Icon>
)}
<div className="mr-auto">{props.itemLabel}</div>
<Icon name="content-next"></Icon>
</div>
);
};
export default SubMenu;
@@ -0,0 +1,129 @@
import { DividerItem, HeaderItem, Item, ItemPanel, Menu, SubMenu } from '..';
import InputRange from '../../InputRange/index.js';
import SwitchButton from '../../SwitchButton/index.js';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
component: Menu,
title: 'Components/AllInOneMenu',
};
<Meta
title="Components/AllInOneMenu"
component={Menu}
/>
export const AllInOneMenuTemplate = args => (
<div className="w-80">
<Menu {...args}>
<ItemPanel>
<Item
label="Item 1"
key="0"
onClick={() => console.info('Item 1 clicked.')}
></Item>
<Item
label="Item 2"
secondaryLabel="Alt item 2"
key="1"
></Item>
<div
className="all-in-one-menu-item py-1"
style={{ flexBasis: 'content' }}
>
<div>Arbitrary item component:</div>
<InputRange
minValue={1}
maxValue={10}
value={5}
onChange={() => {}}
></InputRange>
</div>
<DividerItem key="2"></DividerItem>
<SubMenu
key="3"
backLabel="Back to Level 2"
itemLabel="Item 3 opens a sub menu"
onClick={() => console.info('Sub menu item clicked.')}
showHeaderDivider={true}
headerComponent={
<div className="all-in-one-menu-item flex w-full justify-center">
<SwitchButton label="Switch item in header" />
</div>
}
>
<ItemPanel
index={0}
label="Panel A"
>
<Item label="Panel A item"></Item>
</ItemPanel>
<ItemPanel
index={1}
label="Panel B"
>
<Item label="Panel B item"></Item>
<SubMenu
itemLabel="Opens a sub, sub menu"
headerComponent={<HeaderItem>Header for scrolling list of items</HeaderItem>}
>
<ItemPanel label="Sub sub Panel">
<Item label="Sub sub menu item 1"></Item>
<Item label="Sub sub menu item 2"></Item>
<Item label="Sub sub menu item 3"></Item>
<Item label="Sub sub menu item 4"></Item>
<Item label="Sub sub menu item 5"></Item>
<Item label="Sub sub menu item 6"></Item>
</ItemPanel>
</SubMenu>
</ItemPanel>
</SubMenu>
</ItemPanel>
</Menu>
</div>
);
<Heading
title="AllInOneMenu"
componentRelativePath="AllInOneMenu/AllInOneMenu.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
AllInOneMenu is a component that renders a menu with various menu items, sub-menus and
sub-components. The particular feature of the AllInOneMenu is its ability to render a sub-menu by
replacing the parent menu on screen. It also provides the ability to return to the parent menu with
a back menu item from the sub-menu. Furthermore, each menu level can be split into item panes - that
is several panes of menu items that are switched in and out of view like a tabbed pane.
<Canvas>
<Story
name="Overview"
args={{
className: 'max-h-[210px]',
isVisible: true,
preventHideMenu: true,
backLabel: 'Back to Level 1',
onVisibilityChange: (isVisible) => console.info(`The menu visibility: ${isVisible}`),
}}
>
{AllInOneMenuTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={Menu} />
## Usage
## Contribute
<Footer componentRelativePath="AllInOneMenu/__stories__/AllInOneMenu.stories.mdx" />
@@ -0,0 +1,11 @@
.all-in-one-menu-item {
@apply h-8 px-2 text-[14px] w-full;
display: flex;
align-items: center;
flex-shrink: 0;
line-height: 18px;
}
.all-in-one-menu-item-effects {
@apply cursor-pointer hover:bg-primary-dark hover:rounded;
}
@@ -0,0 +1,19 @@
import Menu, { HorizontalDirection, VerticalDirection } from './Menu';
import DividerItem from './DividerItem';
import HeaderItem from './HeaderItem';
import IconMenu from './IconMenu';
import Item from './Item';
import ItemPanel from './ItemPanel';
import SubMenu from './SubMenu';
export {
Menu,
DividerItem,
HeaderItem,
IconMenu,
Item,
ItemPanel,
SubMenu,
HorizontalDirection,
VerticalDirection,
};
@@ -41,7 +41,7 @@ const ButtonGroup = ({
return cloneElement(child, {
key: index,
className: classnames(
'rounded-[4px] px-2 py-1',
'rounded-[4px] px-2 py-1 text-center',
index === activeIndex
? 'bg-customblue-40 text-white'
: 'text-primary-active bg-black',
+31 -27
View File
@@ -8,6 +8,8 @@ import Svg from '../Svg';
import Icon from '../Icon';
import IconButton from '../IconButton';
import Dropdown from '../Dropdown';
import HeaderPatientInfo from '../HeaderPatientInfo';
import { PatientInfoVisibility } from '../../types/PatientInfoVisibility';
function Header({
children,
@@ -16,6 +18,8 @@ function Header({
onClickReturnButton,
isSticky,
WhiteLabeling,
showPatientInfo = PatientInfoVisibility.VISIBLE_COLLAPSED,
servicesManager,
...props
}): ReactNode {
const { t } = useTranslation('Header');
@@ -34,7 +38,7 @@ function Header({
{...props}
>
<div className="relative h-[48px] items-center ">
<div className="absolute left-0 top-1/2 -translate-y-1/2 ">
<div className="absolute left-0 top-1/2 flex -translate-y-1/2 items-center">
<div
className={classNames(
'mr-3 inline-flex items-center',
@@ -49,7 +53,7 @@ function Header({
className="text-primary-active w-8"
/>
)}
<div className="ml-4">
<div className="ml-1">
{WhiteLabeling?.createLogoComponentFn?.(React, props) || <Svg name="logo-ohif" />}
</div>
</div>
@@ -58,32 +62,30 @@ function Header({
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform">
<div className="flex items-center justify-center space-x-2">{children}</div>
</div>
<div className="absolute right-0 top-1/2 -translate-y-1/2 ">
<Dropdown
id="options"
showDropdownIcon={false}
list={menuOptions}
alignment="right"
>
<IconButton
id={'options-settings-icon'}
variant="text"
color="inherit"
size="initial"
className="text-primary-active"
<div className="absolute right-0 top-1/2 flex -translate-y-1/2 select-none items-center">
{(showPatientInfo === PatientInfoVisibility.VISIBLE ||
showPatientInfo === PatientInfoVisibility.VISIBLE_COLLAPSED) && (
<HeaderPatientInfo servicesManager={servicesManager} />
)}
<div className="border-primary-dark mx-1.5 h-[25px] border-r"></div>
<div className="flex-shrink-0">
<Dropdown
id="options"
showDropdownIcon={false}
list={menuOptions}
alignment="right"
>
<Icon name="settings" />
</IconButton>
<IconButton
id={'options-chevron-down-icon'}
variant="text"
color="inherit"
size="initial"
className="text-primary-active"
>
<Icon name="chevron-down" />
</IconButton>
</Dropdown>
<IconButton
id={'options-settings-icon'}
variant="text"
color="inherit"
size="initial"
className="text-primary-active hover:bg-primary-dark h-full w-full"
>
<Icon name="icon-settings" />
</IconButton>
</Dropdown>
</div>
</div>
</div>
</NavBar>
@@ -103,6 +105,8 @@ Header.propTypes = {
isSticky: PropTypes.bool,
onClickReturnButton: PropTypes.func,
WhiteLabeling: PropTypes.object,
showPatientInfo: PropTypes.string,
servicesManager: PropTypes.object,
};
Header.defaultProps = {
@@ -0,0 +1,134 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Icon } from '@ohif/ui';
import { utils } from '@ohif/core';
import { useAppConfig } from '@state';
const { formatDate, formatPN } = utils;
const formatWithEllipsis = (str, maxLength) => {
if (str.length > maxLength) {
return str.substring(0, maxLength) + '...';
}
return str;
};
function usePatientInfo(servicesManager) {
const { displaySetService } = servicesManager.services;
const [patientInfo, setPatientInfo] = useState({
PatientName: '',
PatientID: '',
PatientSex: '',
PatientDOB: '',
});
const [isMixedPatients, setIsMixedPatients] = useState(false);
const displaySets = displaySetService.getActiveDisplaySets();
const checkMixedPatients = PatientID => {
const displaySets = displaySetService.getActiveDisplaySets();
let isMixedPatients = false;
displaySets.forEach(displaySet => {
const instance = displaySet?.instances?.[0] || displaySet?.instance;
if (!instance) {
return;
}
if (instance.PatientID !== PatientID) {
isMixedPatients = true;
}
});
setIsMixedPatients(isMixedPatients);
};
const updatePatientInfo = () => {
const displaySet = displaySets[0];
const instance = displaySet?.instances?.[0] || displaySet?.instance;
if (!instance) {
return;
}
setPatientInfo({
PatientID: instance.PatientID || '',
PatientName: instance.PatientName ? formatPN(instance.PatientName.Alphabetic) : '',
PatientSex: instance.PatientSex || '',
PatientDOB: formatDate(instance.PatientBirthDate) || '',
});
checkMixedPatients(instance.PatientID || '');
};
useEffect(() => {
const subscription = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
() => updatePatientInfo()
);
return () => subscription.unsubscribe();
}, []);
useEffect(() => {
updatePatientInfo();
}, [displaySets]);
return { patientInfo, isMixedPatients };
}
function HeaderPatientInfo({ servicesManager }) {
const [appConfig] = useAppConfig();
const initialExpandedState = appConfig.showPatientInfo === 'visible';
const [expanded, setExpanded] = useState(initialExpandedState);
const { patientInfo, isMixedPatients } = usePatientInfo(servicesManager);
useEffect(() => {
if (isMixedPatients && expanded) {
setExpanded(false);
}
}, [isMixedPatients, expanded]);
const handleOnClick = () => {
if (!isMixedPatients) {
setExpanded(!expanded);
}
};
const formattedPatientName = formatWithEllipsis(patientInfo.PatientName, 27);
const formattedPatientID = formatWithEllipsis(patientInfo.PatientID, 15);
return (
<div
className="align-items-center hover:bg-primary-dark flex cursor-pointer justify-center gap-1 rounded-lg"
onClick={handleOnClick}
>
<Icon
name={isMixedPatients ? 'icon-multiple-patients' : 'icon-patient'}
className="text-primary-active"
/>
<div className="flex flex-col justify-center">
{expanded ? (
<>
<div className="self-start text-[13px] font-bold text-white">
{formattedPatientName}
</div>
<div className="text-aqua-pale flex gap-2 text-[11px]">
<div>{formattedPatientID}</div>
<div>{patientInfo.PatientSex}</div>
<div>{patientInfo.PatientDOB}</div>
</div>
</>
) : (
<div className="text-primary-active self-center text-[13px]">
{' '}
{isMixedPatients ? 'Multiple Patients' : 'Patient'}
</div>
)}
</div>
<Icon
name="icon-chevron-patient"
className={`text-primary-active ${expanded ? 'rotate-180' : ''}`}
/>
</div>
);
}
HeaderPatientInfo.propTypes = {
servicesManager: PropTypes.object.isRequired,
};
export default HeaderPatientInfo;
@@ -0,0 +1,3 @@
import HeaderPatientInfo from './HeaderPatientInfo';
export default HeaderPatientInfo;
+97 -5
View File
@@ -17,6 +17,8 @@ import chevronMenu from './../../assets/icons/chevron-menu.svg';
import chevronNext from './../../assets/icons/chevron-next.svg';
import chevronPrev from './../../assets/icons/chevron-prev.svg';
import chevronRight from './../../assets/icons/chevron-right.svg';
import contentNext from './../../assets/icons/content-next.svg';
import contentPrev from './../../assets/icons/content-prev.svg';
import eyeVisible from './../../assets/icons/eye-visible.svg';
import eyeHidden from './../../assets/icons/eye-hidden.svg';
import exclamation from './../../assets/icons/exclamation.svg';
@@ -83,6 +85,9 @@ import notificationsInfo from './../../assets/icons/notifications-info.svg';
import notificationsWarning from './../../assets/icons/notifications-warning.svg';
import notificationsError from './../../assets/icons/notifications-error.svg';
import notificationsSuccess from './../../assets/icons/notifications-success.svg';
import nextArrow from './../../assets/icons/next-arrow.svg';
import prevArrow from './../../assets/icons/prev-arrow.svg';
import viewportStatusTracked from './../../assets/icons/viewport-status-tracked.svg';
/** Tools */
import toggleDicomOverlay from './../../assets/icons/toggle-dicom-overlay.svg';
@@ -139,12 +144,53 @@ import iconToolEraser from './../../assets/icons/icon-tool-eraser.svg';
import iconToolScissor from './../../assets/icons/icon-tool-scissor.svg';
import iconToolShape from './../../assets/icons/icon-tool-shape.svg';
import iconToolThreshold from './../../assets/icons/icon-tool-threshold.svg';
import viewportWindowLevel from './../../assets/icons/viewport-window-level.svg';
/** Old OHIF */
import oldTrash from './../../assets/icons/old-trash.svg';
import oldPlay from './../../assets/icons/old-play.svg';
import oldStop from './../../assets/icons/old-stop.svg';
/** ColorLut */
import iconColorLUT from './../../assets/icons/icon-color-lut.svg';
/** New Patient Info Toolbar */
import iconChevronPatient from './../../assets/icons/icon-chevron-patient.svg';
import iconPatient from './../../assets/icons/icon-patient.svg';
import iconSettings from './../../assets/icons/icon-settings.svg';
import iconToolbarBack from './../../assets/icons/icon-toolbar-back.svg';
import iconMultiplePatients from './../../assets/icons/icon-multiple-patients.svg';
/** Volume Rendering */
import CTAAA from './../../assets/icons/CT-AAA.png';
import CTAAA2 from './../../assets/icons/CT-AAA2.png';
import CTAir from './../../assets/icons/CT-Air.png';
import CTBone from './../../assets/icons/CT-Bone.png';
import CTBones from './../../assets/icons/CT-Bones.png';
import CTCardiac from './../../assets/icons/CT-Cardiac.png';
import CTCardiac2 from './../../assets/icons/CT-Cardiac2.png';
import CTCardiac3 from './../../assets/icons/CT-Cardiac3.png';
import CTChestContrastEnhanced from './../../assets/icons/CT-Chest-Contrast-Enhanced.png';
import CTChestVessels from './../../assets/icons/CT-Chest-Vessels.png';
import CTCoronaryArteries from './../../assets/icons/CT-Coronary-Arteries.png';
import CTCoronaryArteries2 from './../../assets/icons/CT-Coronary-Arteries-2.png';
import CTCoronaryArteries3 from './../../assets/icons/CT-Coronary-Arteries-3.png';
import CTCroppedVolumeBone from './../../assets/icons/CT-Cropped-Volume-Bone.png';
import CTFat from './../../assets/icons/CT-Fat.png';
import CTLiverVasculature from './../../assets/icons/CT-Liver-Vasculature.png';
import CTLung from './../../assets/icons/CT-Lung.png';
import CTMIP from './../../assets/icons/CT-MIP.png';
import CTMuscle from './../../assets/icons/CT-Muscle.png';
import CTPulmonaryArteries from './../../assets/icons/CT-Pulmonary-Arteries.png';
import CTSoftTissue from './../../assets/icons/CT-Soft-Tissue.png';
import DTIFABrain from './../../assets/icons/DTI-FA-Brain.png';
import MRAngio from './../../assets/icons/MR-Angio.png';
import MRDefault from './../../assets/icons/MR-Default.png';
import MRMIP from './../../assets/icons/MR-MIP.png';
import MRT2Brain from './../../assets/icons/MR-T2-Brain.png';
import VolumeRendering from './../../assets/icons/VolumeRendering.png';
import actionNewDialog from './../../assets/icons/action-new-dialog.svg';
/** LAYOUT */
import layoutAdvanced3DFourUp from './../../assets/icons/layout-advanced-3d-four-up.svg';
@@ -160,7 +206,6 @@ import layoutCommon2x3 from './../../assets/icons/layout-common-2x3.svg';
import iconToolRotate from './../../assets/icons/tool-3d-rotate.svg';
/** New investigational use */
import investigationalUse from './../../assets/icons/illustration-investigational-use.svg';
const ICONS = {
@@ -180,6 +225,8 @@ const ICONS = {
'chevron-next': chevronNext,
'chevron-prev': chevronPrev,
'chevron-right': chevronRight,
'content-next': contentNext,
'content-prev': contentPrev,
'eye-visible': eyeVisible,
'eye-hidden': eyeHidden,
'external-link': externalLink,
@@ -304,12 +351,51 @@ const ICONS = {
'icon-tool-scissor': iconToolScissor,
'icon-tool-shape': iconToolShape,
'icon-tool-threshold': iconToolThreshold,
'next-arrow': nextArrow,
'prev-arrow': prevArrow,
'viewport-status-tracked': viewportStatusTracked,
'viewport-window-level': viewportWindowLevel,
/** Old OHIF */
'old-trash': oldTrash,
'old-play': oldPlay,
'old-stop': oldStop,
/** ColorLut */
'icon-color-lut': iconColorLUT,
/** New Patient Info Toolbar */
'icon-chevron-patient': iconChevronPatient,
'icon-patient': iconPatient,
'icon-settings': iconSettings,
'icon-toolbar-back': iconToolbarBack,
'icon-multiple-patients': iconMultiplePatients,
/** Volume Rendering */
'CT-AAA': CTAAA,
'CT-AAA2': CTAAA2,
'CT-Air': CTAir,
'CT-Bone': CTBone,
'CT-Bones': CTBones,
'CT-Cardiac': CTCardiac,
'CT-Cardiac2': CTCardiac2,
'CT-Cardiac3': CTCardiac3,
'CT-Chest-Contrast-Enhanced': CTChestContrastEnhanced,
'CT-Chest-Vessels': CTChestVessels,
'CT-Coronary-Arteries': CTCoronaryArteries,
'CT-Coronary-Arteries-2': CTCoronaryArteries2,
'CT-Coronary-Arteries-3': CTCoronaryArteries3,
'CT-Cropped-Volume-Bone': CTCroppedVolumeBone,
'CT-Fat': CTFat,
'CT-Liver-Vasculature': CTLiverVasculature,
'CT-Lung': CTLung,
'CT-MIP': CTMIP,
'CT-Muscle': CTMuscle,
'CT-Pulmonary-Arteries': CTPulmonaryArteries,
'CT-Soft-Tissue': CTSoftTissue,
'DTI-FA-Brain': DTIFABrain,
'MR-Angio': MRAngio,
'MR-Default': MRDefault,
'MR-MIP': MRMIP,
'MR-T2-Brain': MRT2Brain,
VolumeRendering: VolumeRendering,
'action-new-dialog': actionNewDialog,
/** LAYOUT */
'layout-advanced-3d-four-up': layoutAdvanced3DFourUp,
'layout-advanced-3d-main': layoutAdvanced3DMain,
@@ -340,11 +426,17 @@ function addIcon(iconName, iconSVG) {
* return `null`
*/
export default function getIcon(key, props) {
if (!key || !ICONS[key]) {
const icon = ICONS[key];
if (!key || !icon) {
return React.createElement('div', null, 'Missing Icon');
}
return React.createElement(ICONS[key], props);
if (typeof icon === 'string' && icon.endsWith('.png')) {
return React.createElement('img', { src: icon, ...props });
} else {
return React.createElement(icon, props);
}
}
export { getIcon, ICONS, addIcon };
@@ -1,13 +1,13 @@
.scroll {
height: 100%;
height: calc(100% - 30px);
padding: 5px;
position: absolute;
right: 0;
top: 0;
top: 30px;
}
.scroll .scroll-holder {
height: calc(100% - 20px);
margin-top: 5px;
height: calc(100%);
margin-bottom: 5px;
position: relative;
width: 12px;
}
@@ -12,6 +12,12 @@ import getMaxDigits from '../../utils/getMaxDigits';
*
*
*/
type Label = {
text: string;
position: number;
};
type InputRangeProps = {
value: number;
onChange: (value: number) => void;
@@ -28,6 +34,7 @@ type InputRangeProps = {
trackColor?: string;
allowNumberEdit?: boolean;
showAdjustmentArrows?: boolean;
labels?: Label[];
};
const InputRange: React.FC<InputRangeProps> = ({
@@ -46,6 +53,7 @@ const InputRange: React.FC<InputRangeProps> = ({
trackColor,
allowNumberEdit = false,
showAdjustmentArrows = true,
labels = [],
}) => {
const [rangeValue, setRangeValue] = useState(value);
@@ -114,6 +122,22 @@ const InputRange: React.FC<InputRangeProps> = ({
{showLabel && labelPosition === 'right' && (
<div style={{ width: labelWidth }}>{LabelOrEditableNumber}</div>
)}
{labels.length > 0 && (
<>
{labels.map((label, index) => {
const position = label.position;
return (
<div
key={index}
className="absolute !m-0"
style={{ left: `calc(${position}%)`, bottom: '-20px' }}
>
<span className="text-aqua-pale text-xs">{label.text}</span>
</div>
);
})}
</>
)}
</div>
</div>
);
+64 -29
View File
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactModal from 'react-modal';
import Draggable from 'react-draggable';
import { useModal } from '../../contextProviders';
import Icon from '../Icon';
@@ -20,6 +21,9 @@ const Modal = ({
onClose,
children,
shouldCloseOnOverlayClick,
movable = false,
containerDimensions = null,
contentDimensions = null,
}) => {
const { hide } = useModal();
@@ -27,44 +31,70 @@ const Modal = ({
hide();
};
const renderHeader = () => {
return (
title && (
<header className="bg-primary-dark flex items-center rounded-tl rounded-tr px-[20px] py-[13px]">
<Typography
variant="h6"
color="primaryLight"
className="flex grow !leading-[1.2]"
data-cy="modal-header"
>
{title}
</Typography>
{closeButton && (
<Icon
onClick={onClose}
name="close"
className="text-primary-active cursor-pointer"
/>
)}
</header>
)
const renderHeader = () =>
title && (
<header className="bg-primary-dark drag-handle flex items-center rounded-tl rounded-tr px-[20px] py-[13px]">
<Typography
variant="h6"
color="primaryLight"
className="flex grow !leading-[1.2]"
data-cy="modal-header"
>
{title}
</Typography>
{closeButton && (
<Icon
onClick={onClose}
name="close"
className="text-primary-active cursor-pointer"
/>
)}
</header>
);
};
const modalContent = (
<>
{renderHeader()}
<section
className={
contentDimensions
? `ohif-scrollbar bg-primary-dark overflow-y-auto ${contentDimensions}`
: 'ohif-scrollbar modal-content bg-primary-dark overflow-y-auto rounded-bl rounded-br px-[20px] pt-2 pb-[20px]'
}
>
{children}
</section>
</>
);
return (
<ReactModal
className="relative max-h-full w-11/12 text-white outline-none lg:w-10/12 xl:w-9/12"
overlayClassName="fixed top-0 left-0 right-0 bottom-0 z-50 bg-overlay flex items-start justify-center py-16"
className={
containerDimensions
? `relative text-white outline-none ${containerDimensions}`
: 'relative max-h-full w-11/12 text-white outline-none lg:w-10/12 xl:w-9/12'
}
overlayClassName={
movable
? 'fixed top-0 left-0 right-0 bottom-0 z-50 flex items-center justify-center py-16 pointer-events-none'
: 'fixed top-0 left-0 right-0 bottom-0 z-50 bg-overlay flex items-center justify-center py-16'
}
shouldCloseOnEsc={shouldCloseOnEsc}
onRequestClose={handleClose}
isOpen={isOpen}
title={title}
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
>
{renderHeader()}
<section className="ohif-scrollbar modal-content bg-primary-dark overflow-y-auto rounded-bl rounded-br px-[20px] pt-2 pb-[20px]">
{children}
</section>
{movable ? (
<Draggable
handle=".drag-handle"
defaultClassName="bg-primary-dark pointer-events-auto"
>
<div>{modalContent}</div>
</Draggable>
) : (
modalContent
)}
</ReactModal>
);
};
@@ -72,6 +102,9 @@ const Modal = ({
Modal.defaultProps = {
shouldCloseOnEsc: true,
shouldCloseOnOverlayClick: true,
movable: false,
containerDimensions: null,
contentDimensions: null,
};
Modal.propTypes = {
@@ -80,9 +113,11 @@ Modal.propTypes = {
isOpen: PropTypes.bool,
title: PropTypes.string,
onClose: PropTypes.func,
/** The modal's content */
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
shouldCloseOnOverlayClick: PropTypes.bool,
movable: PropTypes.bool,
containerDimensions: PropTypes.string,
contentDimensions: PropTypes.string,
};
export default Modal;
@@ -149,7 +149,6 @@ StudyBrowser.propTypes = {
componentType: PropTypes.oneOf(['thumbnail', 'thumbnailTracked', 'thumbnailNoImage'])
.isRequired,
isTracked: PropTypes.bool,
viewportIdentificator: PropTypes.arrayOf(PropTypes.string),
/**
* Data the thumbnail should expose to a receiving drop target. Use a matching
* `dragData.type` to identify which targets can receive this draggable item.
@@ -99,7 +99,6 @@ With Studies
displaySetInstanceUID: '0b66675a-a4e3-a718-21f4-035d2d178d62',
},
isTracked: false,
viewportIdentificator: [],
},
],
},
+1 -1
View File
@@ -1,6 +1,6 @@
import React from 'react';
// Svgs
import logoOhif from './../../assets/svgs/logo-ohif.svg';
import logoOhif from './../../assets/svgs/ohif-logo.svg';
const SVGS = {
'logo-ohif': logoOhif,
@@ -0,0 +1,52 @@
import React, { useCallback, useState } from 'react';
import './switchButton.css';
export enum SwitchLabelLocation {
left,
right,
}
export type SwitchButtonProps = {
checked?: boolean;
label?: string;
labelLocation?: SwitchLabelLocation;
onChange?: (checked: boolean) => void;
};
const SwitchButton = ({
label,
checked = false,
onChange,
labelLocation = SwitchLabelLocation.left,
}: SwitchButtonProps) => {
const [isInputChecked, setIsInputChecked] = useState(checked);
const onHandleChange = useCallback(
event => {
setIsInputChecked(event.target.checked);
onChange?.(event.target.checked);
},
[onChange]
);
// Thanks goes to https://codepen.io/lhermann/pen/EBGZRZ for the inspiration to the code below.
return (
<label className="switch-button flex w-full cursor-pointer items-center justify-between text-[14px]">
{label && labelLocation === SwitchLabelLocation.left && <div>{label}</div>}
<div className="relative">
<input
className="absolute hidden"
type="checkbox"
onChange={onHandleChange}
checked={isInputChecked}
/>
<div className="switch-button-outer border-common-bright bg-primary-dark block h-[16px] w-[30px] rounded-full border"></div>
<div className="switch-button-dot bg-common-bright absolute left-[4px] top-[3px] h-[10px] w-[10px] rounded-full transition duration-150 ease-in-out"></div>
</div>
{label && labelLocation === SwitchLabelLocation.right && <div>{label}</div>}
</label>
);
};
export default SwitchButton;
@@ -0,0 +1,58 @@
import SwitchButton, { SwitchLabelLocation } from '../SwitchButton';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
export const argTypes = {
component: SwitchButton,
title: 'Components/SwitchButton',
};
<Meta
title="Components/SwitchButton"
component={SwitchButton}
/>
export const SwitchButtonTemplate = args => (
<div className="w-[200px]">
<SwitchButton {...args} />
</div>
);
<Heading
title="SwitchButton"
componentRelativePath="SwitchButton/SwitchButton.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
A SwitchButton is a checkbox type input/button that can either be on or off (like a light switch).
<Canvas>
<Story
name="Overview"
args={{
label: 'Switch Button Label',
labelLocation: SwitchLabelLocation.left,
onChange: (checked) => console.info(`${checked ? 'The switch is on.' : 'The switch is off.'}`)
}}
>
{SwitchButtonTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={SwitchButton} />
## Usage
## Contribute
<Footer componentRelativePath="SwitchButton/__stories__/SwitchButton.stories.mdx" />
@@ -0,0 +1,3 @@
import SwitchButton, { SwitchLabelLocation } from './SwitchButton';
export default SwitchButton;
export { SwitchLabelLocation };
@@ -0,0 +1,7 @@
.switch-button input:checked ~ .switch-button-outer {
@apply border-primary-light;
}
.switch-button input:checked ~ .switch-button-dot {
@apply bg-primary-light;
transform: translateX(12px);
}
@@ -29,7 +29,6 @@ const ThumbnailList = ({
componentType,
seriesDate,
countIcon,
viewportIdentificator,
isTracked,
canReject,
onReject,
@@ -53,7 +52,6 @@ const ThumbnailList = ({
imageSrc={imageSrc}
imageAltText={imageAltText}
messages={messages}
viewportIdentificator={viewportIdentificator}
isActive={isActive}
onClick={() => onThumbnailClick(displaySetInstanceUID)}
onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)}
@@ -72,7 +70,6 @@ const ThumbnailList = ({
imageSrc={imageSrc}
imageAltText={imageAltText}
messages={messages}
viewportIdentificator={viewportIdentificator}
isTracked={isTracked}
isActive={isActive}
onClick={() => onThumbnailClick(displaySetInstanceUID)}
@@ -96,7 +93,6 @@ const ThumbnailList = ({
onReject={onReject}
onClick={() => onThumbnailClick(displaySetInstanceUID)}
onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)}
viewportIdentificator={viewportIdentificator}
isHydratedForDerivedDisplaySet={isHydratedForDerivedDisplaySet}
/>
);
@@ -120,7 +116,6 @@ ThumbnailList.propTypes = {
numInstances: PropTypes.number,
description: PropTypes.string,
componentType: Types.ThumbnailType.isRequired,
viewportIdentificator: Types.StringArray,
isTracked: PropTypes.bool,
/**
* Data the thumbnail should expose to a receiving drop target. Use a matching
@@ -24,43 +24,11 @@ function ThumbnailTracked({
onClick,
onDoubleClick,
onClickUntrack,
viewportIdentificator,
isTracked,
isActive,
}) {
const { t } = useTranslation('ThumbnailTracked');
const trackedIcon = isTracked ? 'circled-checkmark' : 'dotted-circle';
const viewportIdentificatorLabel = viewportIdentificator.join(', ');
const renderViewportLabels = () => {
const MAX_LABELS_PER_COL = 3;
const shouldShowStack = viewportIdentificator.length > MAX_LABELS_PER_COL;
if (shouldShowStack) {
return (
<div>
<div>
{viewportIdentificator.slice(0, MAX_LABELS_PER_COL).map(label => (
<div key={label}>{label}</div>
))}
</div>
<Tooltip
position="right"
content={
<div className="max-w-40 text-left">
{`${t('Viewport')}: ${viewportIdentificatorLabel}`}
</div>
}
>
<Icon
name="tool-more-menu"
className="py-2 text-white"
/>
</Tooltip>
</div>
);
}
return viewportIdentificator.map(label => <div key={label}>{label}</div>);
};
return (
<div
@@ -93,9 +61,6 @@ function ThumbnailTracked({
{isTracked ? t('Series is tracked') : t('Series is untracked')}
</span>
</span>
{!!viewportIdentificator.length && (
<span>{`${t('Viewport')}: ${viewportIdentificatorLabel}`}</span>
)}
</div>
</div>
}
@@ -105,13 +70,6 @@ function ThumbnailTracked({
className="text-primary-light mb-2 w-4"
/>
</Tooltip>
<div
className="text-center text-xl leading-tight text-white"
data-cy={'thumbnail-viewport-labels'}
>
{renderViewportLabels()}
</div>
</div>
{isTracked && (
<div onClick={onClickUntrack}>
@@ -162,7 +120,6 @@ ThumbnailTracked.propTypes = {
onClick: PropTypes.func.isRequired,
onDoubleClick: PropTypes.func.isRequired,
onClickUntrack: PropTypes.func.isRequired,
viewportIdentificator: PropTypes.array,
isTracked: PropTypes.bool,
messages: PropTypes.object,
isActive: PropTypes.bool.isRequired,
@@ -0,0 +1,33 @@
import React from 'react';
import Icon from '../Icon';
import classNames from 'classnames';
const arrowClasses =
'cursor-pointer flex items-center justify-center shrink-0 text-primary-light active:text-white hover:bg-secondary-light/60 rounded';
type ViewportActionArrowsProps = {
onArrowsClick: (direction: number) => void;
className: string;
};
function ViewportActionArrows({ onArrowsClick, className }: ViewportActionArrowsProps) {
return (
// The arrows are only visible when hover over the viewport.
<div className={classNames(className, 'flex')}>
<div className={arrowClasses}>
<Icon
name="prev-arrow"
onClick={() => onArrowsClick(-1)}
/>
</div>
<div className={arrowClasses}>
<Icon
name="next-arrow"
onClick={() => onArrowsClick(1)}
/>
</div>
</div>
);
}
export default ViewportActionArrows;
@@ -0,0 +1,2 @@
import ViewportActionArrows from './ViewportActionArrows';
export default ViewportActionArrows;
@@ -0,0 +1,76 @@
import classNames from 'classnames';
import React from 'react';
import { ViewportActionCornersComponentInfo } from '../../types/ViewportActionCornersTypes';
export enum ViewportActionCornersLocations {
topLeft,
topRight,
bottomLeft,
bottomRight,
}
export type ViewportActionCornersProps = {
cornerComponents: Record<
ViewportActionCornersLocations,
Array<ViewportActionCornersComponentInfo>
>;
};
const commonClasses = 'pointer-events-auto flex items-center gap-1';
const classes = {
[ViewportActionCornersLocations.topLeft]: classNames(
commonClasses,
'absolute top-[4px] left-[0px] pl-[4px]'
),
[ViewportActionCornersLocations.topRight]: classNames(
commonClasses,
'absolute top-[4px] right-[4px] right-viewport-scrollbar'
),
[ViewportActionCornersLocations.bottomLeft]: classNames(
commonClasses,
'absolute bottom-[4px] left-[0px] pl-[4px]'
),
[ViewportActionCornersLocations.bottomRight]: classNames(
commonClasses,
'absolute bottom-[4px] right-[0px] right-viewport-scrollbar'
),
};
/**
* A component that renders various action items/components to each corner of a viewport.
* The position of each corner's components is such that a single row of components are
* rendered absolutely without intersecting the ViewportOverlay component.
* Note that corner components are passed as an object mapping each corner location
* to an array of components for that location. The components in each array are
* rendered from left to right in the order that they appear in the array.
*/
function ViewportActionCorners({ cornerComponents }: ViewportActionCornersProps) {
if (!cornerComponents) {
return null;
}
return (
<div
className="pointer-events-none absolute h-full w-full select-none"
onDoubleClick={event => {
event.preventDefault();
event.stopPropagation();
}}
>
{Object.entries(cornerComponents).map(([location, locationComponents]) => {
return (
<div
key={location}
className={classNames(classes[location])}
>
{locationComponents.map(componentInfo => {
return <div key={componentInfo.id}>{componentInfo.component}</div>;
})}
</div>
);
})}
</div>
);
}
export default ViewportActionCorners;
@@ -0,0 +1,4 @@
import ViewportActionCorners, { ViewportActionCornersLocations } from './ViewportActionCorners';
export { ViewportActionCornersLocations };
export default ViewportActionCorners;
@@ -10,40 +10,13 @@
text-shadow: 1px 1px #000;
pointer-events: none;
}
.ViewportOverlay .top-left {
top: 20px;
left: 20px;
.overlay-top {
top: 28px;
}
.ViewportOverlay .top-center {
top: 20px;
padding-top: 20px;
width: 100%;
text-align: center;
.overlay-bottom {
bottom: 3px;
}
.ViewportOverlay .top-right {
top: 20px;
right: 20px;
text-align: right;
}
.ViewportOverlay .bottom-left {
bottom: 20px;
left: 20px;
}
.ViewportOverlay .bottom-right {
bottom: 20px;
right: 20px;
text-align: right;
}
.ViewportOverlay.controlsVisible .topright,
.ViewportOverlay.controlsVisible .bottomright {
right: calc(20px + 19px);
}
.ViewportOverlay svg {
color: #9ccef9;
fill: #9ccef9;
stroke: #9ccef9;
background-color: transparent;
margin: 2px;
width: 18px;
height: 18px;
.overlay-text {
text-shadow: 0.8px 0.8px 0.5px rgba(0, 0, 0, 0.75);
}
@@ -3,11 +3,16 @@ import classnames from 'classnames';
import './ViewportOverlay.css';
// The overlay-top and overlay-bottom classes are explicitly needed to offset
// the overlays (i.e. via absolute positioning) such the ViewportActionCorners
// have space for its child components.
// ToDo: offset the ViewportOverlay automatically via css to account for the
// space needed for ViewportActionCorners.
const classes = {
topLeft: 'top-viewport left-viewport',
topRight: 'top-viewport right-viewport-scrollbar',
bottomRight: 'bottom-viewport right-viewport-scrollbar',
bottomLeft: 'bottom-viewport left-viewport',
topLeft: 'overlay-top left-viewport',
topRight: 'overlay-top right-viewport-scrollbar',
bottomRight: 'overlay-bottom right-viewport-scrollbar',
bottomLeft: 'overlay-bottom left-viewport',
};
export type ViewportOverlayProps = {
@@ -27,7 +32,14 @@ const ViewportOverlay = ({
}: ViewportOverlayProps) => {
const overlay = 'absolute pointer-events-none viewport-overlay';
return (
<div className={color}>
<div
className={classnames(
color ? color : 'text-aqua-pale',
'text-[13px]',
'leading-5',
'overlay-text'
)}
>
<div
data-cy={'viewport-overlay-top-left'}
className={classnames(overlay, classes.topLeft)}
@@ -37,12 +49,14 @@ const ViewportOverlay = ({
<div
data-cy={'viewport-overlay-top-right'}
className={classnames(overlay, classes.topRight)}
style={{ transform: 'translateX(-8px)' }} // shift right side overlays by 4px for better alignment with ViewportActionCorners' icons
>
{topRight}
</div>
<div
data-cy={'viewport-overlay-bottom-right'}
className={classnames(overlay, classes.bottomRight)}
style={{ transform: 'translateX(-8px)' }} // shift right side overlays by 4px for better alignment with ViewportActionCorners' icons
>
{bottomRight}
</div>
@@ -64,7 +64,7 @@ function ViewportPane({
onScroll={onInteractionHandler}
onWheel={onInteractionHandler}
className={classnames(
'hover:border-primary-light group h-full w-full overflow-hidden rounded-md transition duration-300',
'group h-full w-full overflow-hidden rounded-md transition duration-300',
{
'border-primary-light border-2': isActive,
'border-2 border-transparent': !isActive,
@@ -77,10 +77,10 @@ function ViewportPane({
>
<div
className={classnames(
'h-full w-full overflow-hidden rounded-md group-hover:border-transparent',
'h-full w-full overflow-hidden rounded-md',
{
'border border-transparent': isActive,
'border-secondary-light border': !isActive,
'border-secondary-light group-hover:border-primary-light/70 border': !isActive,
},
className
)}
+13
View File
@@ -73,22 +73,29 @@ import CheckBox from './CheckBox';
import LoadingIndicatorProgress from './LoadingIndicatorProgress';
import LoadingIndicatorTotalPercent from './LoadingIndicatorTotalPercent';
import ViewportActionBar from './ViewportActionBar';
import ViewportActionCorners, { ViewportActionCornersLocations } from './ViewportActionCorners';
import ProgressLoadingBar from './ProgressLoadingBar';
import LegacySidePanel from './LegacySidePanel';
import PanelSection from './PanelSection';
import AdvancedToolbox from './AdvancedToolbox';
import InputDoubleRange from './InputDoubleRange';
import LegacyButtonGroup from './LegacyButtonGroup';
import SwitchButton, { SwitchLabelLocation } from './SwitchButton';
import * as AllInOneMenu from './AllInOneMenu';
import ViewportActionArrows from './ViewportActionArrows';
import HeaderPatientInfo from './HeaderPatientInfo';
import LegacySplitButton from './LegacySplitButton';
import { ToolSettings } from './AdvancedToolbox';
import { Toolbox } from './Toolbox';
import InvestigationalUseDialog from './InvestigationalUseDialog';
import MeasurementItem from './MeasurementTable/MeasurementItem';
import LayoutPreset from './LayoutPreset';
export {
AboutModal,
AdvancedToolbox,
AllInOneMenu,
HotkeyField,
Header,
UserPreferences,
@@ -148,6 +155,8 @@ export {
StudyListTableRow,
StudySummary,
Svg,
SwitchButton,
SwitchLabelLocation,
Table,
TableBody,
TableCell,
@@ -163,12 +172,16 @@ export {
TooltipClipboard,
Typography,
Viewport,
ViewportActionArrows,
ViewportActionBar,
ViewportActionCorners,
ViewportActionCornersLocations,
ViewportDownloadForm,
ViewportGrid,
ViewportPane,
ViewportOverlay,
WindowLevelMenuItem,
HeaderPatientInfo,
LayoutPreset,
LegacySplitButton,
ToolSettings,
@@ -31,6 +31,9 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
closeButton: true,
title: null,
customClassName: '',
movable: false,
containerDimensions: null,
contentDimensions: null,
};
const { t } = useTranslation('Modals');
@@ -73,6 +76,9 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
shouldCloseOnEsc,
closeButton,
shouldCloseOnOverlayClick,
movable,
containerDimensions,
contentDimensions,
} = options;
return (
@@ -86,6 +92,9 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
closeButton={closeButton}
onClose={hide}
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
movable={movable}
containerDimensions={containerDimensions}
contentDimensions={contentDimensions}
>
<ModalContent
{...contentProps}
+7
View File
@@ -33,6 +33,7 @@ export {
export {
AboutModal,
AdvancedToolbox,
AllInOneMenu,
HotkeyField,
Header,
UserPreferences,
@@ -92,6 +93,8 @@ export {
StudyListTableRow,
StudySummary,
Svg,
SwitchButton,
SwitchLabelLocation,
Table,
TableBody,
TableCell,
@@ -107,13 +110,17 @@ export {
TooltipClipboard,
Typography,
Viewport,
ViewportActionArrows,
ViewportActionBar,
ViewportActionCorners,
ViewportActionCornersLocations,
ViewportDownloadForm,
ViewportGrid,
ViewportPane,
WindowLevelMenuItem,
ImageScrollbar,
ViewportOverlay,
HeaderPatientInfo,
ToolSettings,
Toolbox,
InvestigationalUseDialog,
@@ -0,0 +1,5 @@
export enum PatientInfoVisibility {
VISIBLE = 'visible',
VISIBLE_COLLAPSED = 'visibleCollapsed',
DISABLED = 'disabled',
}
@@ -0,0 +1,6 @@
import { ReactNode } from 'react';
export interface ViewportActionCornersComponentInfo {
id: string;
component: ReactNode;
}
+3 -1
View File
@@ -1,10 +1,12 @@
import PropTypes from 'prop-types';
import ThumbnailType from './ThumbnailType';
import { PatientInfoVisibility } from './PatientInfoVisibility';
// A few miscellaneous types declared inline here.
export * from './Predicate';
export * from './ContextMenuItem';
export * from './ViewportActionCornersTypes';
/**
* StringNumber often comes back from DICOMweb for integer valued items.
@@ -17,4 +19,4 @@ const StringNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number]);
*/
const StringArray = PropTypes.oneOfType([PropTypes.string, PropTypes.array]);
export { StringNumber, StringArray, ThumbnailType };
export { StringNumber, StringArray, ThumbnailType, PatientInfoVisibility };
@@ -11,7 +11,6 @@ const studyWithSR = {
seriesNumber: 1,
numInstances: 68,
componentType: 'thumbnailTracked',
viewportIdentificator: 'A',
isTracked: true,
},
{
@@ -20,7 +19,6 @@ const studyWithSR = {
seriesNumber: 1,
numInstances: 68,
componentType: 'thumbnailTracked',
viewportIdentificator: 'B',
},
{
displaySetInstanceUID: 'f69f648c-223e-dasdasdb7f-c4af-b8906641a66e',