diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..31a4f99 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,45 @@ +name: Deploy to Docker Hub and Render + +on: + push: + branches: + - retina # Change to your deployment branch + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + platforms: linux/amd64 + push: true + tags: seanyl/deepgit:app + + deploy-to-render: + needs: build-and-push # Ensure this runs only after the Docker image is pushed + runs-on: ubuntu-latest + + steps: + - name: Trigger Render Deployment + run: | + curl -X POST "$RENDER_DEPLOY_HOOK" + env: + RENDER_DEPLOY_HOOK: ${{ secrets.RENDER_DEPLOY_HOOK }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b29a94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +.ipython_checkpoints diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cbdd815 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM seanyl/deepgit:latest + +WORKDIR /app + +# Remove problematic dependencies and force a fresh install +RUN rm -rf node_modules package-lock.json && \ + npm cache clean --force && \ + npm install --force + +CMD ["npm", "start"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..267c3ab --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + retina + Copyright (C) 2022 OuestWare + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) 2022 OuestWare + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index be790e1..641bdea 100644 --- a/README.md +++ b/README.md @@ -1 +1,24 @@ -# deepgit \ No newline at end of file + +
+ WESE Logo +

DeepGit

+
+ +# Overview +DeepGit is a free, open-source web application designed to help researchers and research software engineers discover and explore research software within specific domains. It is a joint effort between the University of Illinois Urbana-Champaign's [Data Exploration Lab](https://github.com/data-exp-lab) and NumFOCUS's [Map of Open Source Science](https://www.opensource.science/moss) + +

+ + +

+ + + +# Acknowledgment +DeepGit is built upon [Retina](https://ouestware.gitlab.io/retina/1.0.0-beta.4/#/) developed by [OuestWare](https://www.ouestware.com/en/) + +# License +The software is available under [GNU GPLv3 license](https://gitlab.com/ouestware/retina/-/blob/main/LICENSE). + +# Contact +For any queries, please [open an issue](https://github.com/data-exp-lab/deepgit/issues) on GitHub \ No newline at end of file diff --git a/dist/assets/index-B1f_hSLR.css b/dist/assets/index-B1f_hSLR.css new file mode 100644 index 0000000..a58bbb5 --- /dev/null +++ b/dist/assets/index-B1f_hSLR.css @@ -0,0 +1 @@ +@charset "UTF-8";@import"https://fonts.googleapis.com/css2?family=Sanchez&family=Public+Sans&display=swap";.rc-slider{position:relative;width:100%;height:14px;padding:5px 0;border-radius:6px;touch-action:none;box-sizing:border-box;-webkit-tap-highlight-color:rgba(0,0,0,0)}.rc-slider *{box-sizing:border-box;-webkit-tap-highlight-color:rgba(0,0,0,0)}.rc-slider-rail{position:absolute;width:100%;height:4px;background-color:#e9e9e9;border-radius:6px}.rc-slider-track,.rc-slider-tracks{position:absolute;height:4px;background-color:#abe2fb;border-radius:6px}.rc-slider-track-draggable{z-index:1;box-sizing:content-box;background-clip:content-box;border-top:5px solid rgba(0,0,0,0);border-bottom:5px solid rgba(0,0,0,0);transform:translateY(-5px)}.rc-slider-handle{position:absolute;z-index:1;width:14px;height:14px;margin-top:-5px;background-color:#fff;border:solid 2px #96dbfa;border-radius:50%;cursor:pointer;cursor:-webkit-grab;cursor:grab;opacity:.8;-webkit-user-select:none;user-select:none;touch-action:pan-x}.rc-slider-handle-dragging.rc-slider-handle-dragging.rc-slider-handle-dragging{border-color:#57c5f7;box-shadow:0 0 0 5px #96dbfa}.rc-slider-handle-dragging.rc-slider-handle-dragging.rc-slider-handle-dragging-delete{opacity:0}.rc-slider-handle:focus{outline:none;box-shadow:none}.rc-slider-handle:focus-visible{border-color:#2db7f5;box-shadow:0 0 0 3px #96dbfa}.rc-slider-handle-click-focused:focus{border-color:#96dbfa;box-shadow:unset}.rc-slider-handle:hover{border-color:#57c5f7}.rc-slider-handle:active{border-color:#57c5f7;box-shadow:0 0 5px #57c5f7;cursor:-webkit-grabbing;cursor:grabbing}.rc-slider-mark{position:absolute;top:18px;left:0;width:100%;font-size:12px}.rc-slider-mark-text{position:absolute;display:inline-block;color:#999;text-align:center;vertical-align:middle;cursor:pointer}.rc-slider-mark-text-active{color:#666}.rc-slider-step{position:absolute;width:100%;height:4px;background:transparent}.rc-slider-dot{position:absolute;bottom:-2px;width:8px;height:8px;vertical-align:middle;background-color:#fff;border:2px solid #e9e9e9;border-radius:50%;cursor:pointer}.rc-slider-dot-active{border-color:#96dbfa}.rc-slider-dot-reverse{margin-right:-4px}.rc-slider-disabled{background-color:#e9e9e9}.rc-slider-disabled .rc-slider-track{background-color:#ccc}.rc-slider-disabled .rc-slider-handle,.rc-slider-disabled .rc-slider-dot{background-color:#fff;border-color:#ccc;box-shadow:none;cursor:not-allowed}.rc-slider-disabled .rc-slider-mark-text,.rc-slider-disabled .rc-slider-dot{cursor:not-allowed!important}.rc-slider-vertical{width:14px;height:100%;padding:0 5px}.rc-slider-vertical .rc-slider-rail{width:4px;height:100%}.rc-slider-vertical .rc-slider-track{bottom:0;left:5px;width:4px}.rc-slider-vertical .rc-slider-track-draggable{border-top:0;border-right:5px solid rgba(0,0,0,0);border-bottom:0;border-left:5px solid rgba(0,0,0,0);transform:translate(-5px)}.rc-slider-vertical .rc-slider-handle{position:absolute;z-index:1;margin-top:0;margin-left:-5px;touch-action:pan-y}.rc-slider-vertical .rc-slider-mark{top:0;left:18px;height:100%}.rc-slider-vertical .rc-slider-step{width:4px;height:100%}.rc-slider-vertical .rc-slider-dot{margin-left:-2px}.rc-slider-tooltip-zoom-down-enter,.rc-slider-tooltip-zoom-down-appear,.rc-slider-tooltip-zoom-down-leave{display:block!important;animation-duration:.3s;animation-fill-mode:both;animation-play-state:paused}.rc-slider-tooltip-zoom-down-enter.rc-slider-tooltip-zoom-down-enter-active,.rc-slider-tooltip-zoom-down-appear.rc-slider-tooltip-zoom-down-appear-active{animation-name:rcSliderTooltipZoomDownIn;animation-play-state:running}.rc-slider-tooltip-zoom-down-leave.rc-slider-tooltip-zoom-down-leave-active{animation-name:rcSliderTooltipZoomDownOut;animation-play-state:running}.rc-slider-tooltip-zoom-down-enter,.rc-slider-tooltip-zoom-down-appear{transform:scale(0);animation-timing-function:cubic-bezier(.23,1,.32,1)}.rc-slider-tooltip-zoom-down-leave{animation-timing-function:cubic-bezier(.755,.05,.855,.06)}@keyframes rcSliderTooltipZoomDownIn{0%{transform:scale(0);transform-origin:50% 100%;opacity:0}to{transform:scale(1);transform-origin:50% 100%}}@keyframes rcSliderTooltipZoomDownOut{0%{transform:scale(1);transform-origin:50% 100%}to{transform:scale(0);transform-origin:50% 100%;opacity:0}}.rc-slider-tooltip{position:absolute;top:-9999px;left:-9999px;visibility:visible;box-sizing:border-box;-webkit-tap-highlight-color:rgba(0,0,0,0)}.rc-slider-tooltip *{box-sizing:border-box;-webkit-tap-highlight-color:rgba(0,0,0,0)}.rc-slider-tooltip-hidden{display:none}.rc-slider-tooltip-placement-top{padding:4px 0 8px}.rc-slider-tooltip-inner{min-width:24px;height:24px;padding:6px 2px;color:#fff;font-size:12px;line-height:1;text-align:center;text-decoration:none;background-color:#6c6c6c;border-radius:6px;box-shadow:0 0 4px #d9d9d9}.rc-slider-tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.rc-slider-tooltip-placement-top .rc-slider-tooltip-arrow{bottom:4px;left:50%;margin-left:-4px;border-width:4px 4px 0;border-top-color:#6c6c6c}:root,[data-bs-theme=light]{--bs-blue: #0d6efd;--bs-indigo: #6610f2;--bs-purple: #6f42c1;--bs-pink: #d63384;--bs-red: #dc3545;--bs-orange: #fd7e14;--bs-yellow: #ffc107;--bs-green: #7bd16c;--bs-teal: #20c997;--bs-cyan: #c9dbed;--bs-black: #000;--bs-white: #fff;--bs-gray: #6c757d;--bs-gray-dark: #343a40;--bs-gray-100: #f8f9fa;--bs-gray-200: #e9ecef;--bs-gray-300: #dee2e6;--bs-gray-400: #ced4da;--bs-gray-500: #adb5bd;--bs-gray-600: #6c757d;--bs-gray-700: #495057;--bs-gray-800: #343a40;--bs-gray-900: #212529;--bs-primary: #0d6efd;--bs-secondary: #6c757d;--bs-success: #7bd16c;--bs-info: #c9dbed;--bs-warning: #ffc107;--bs-danger: #dc3545;--bs-light: #f8f9fa;--bs-dark: #212529;--bs-primary-rgb: 13, 110, 253;--bs-secondary-rgb: 108, 117, 125;--bs-success-rgb: 123, 209, 108;--bs-info-rgb: 201, 219, 237;--bs-warning-rgb: 255, 193, 7;--bs-danger-rgb: 220, 53, 69;--bs-light-rgb: 248, 249, 250;--bs-dark-rgb: 33, 37, 41;--bs-primary-text-emphasis: rgb(5.2, 44, 101.2);--bs-secondary-text-emphasis: rgb(43.2, 46.8, 50);--bs-success-text-emphasis: rgb(49.2, 83.6, 43.2);--bs-info-text-emphasis: rgb(80.4, 87.6, 94.8);--bs-warning-text-emphasis: rgb(102, 77.2, 2.8);--bs-danger-text-emphasis: rgb(88, 21.2, 27.6);--bs-light-text-emphasis: #495057;--bs-dark-text-emphasis: #495057;--bs-primary-bg-subtle: rgb(206.6, 226, 254.6);--bs-secondary-bg-subtle: rgb(225.6, 227.4, 229);--bs-success-bg-subtle: rgb(228.6, 245.8, 225.6);--bs-info-bg-subtle: rgb(244.2, 247.8, 251.4);--bs-warning-bg-subtle: rgb(255, 242.6, 205.4);--bs-danger-bg-subtle: rgb(248, 214.6, 217.8);--bs-light-bg-subtle: rgb(251.5, 252, 252.5);--bs-dark-bg-subtle: #ced4da;--bs-primary-border-subtle: rgb(158.2, 197, 254.2);--bs-secondary-border-subtle: rgb(196.2, 199.8, 203);--bs-success-border-subtle: rgb(202.2, 236.6, 196.2);--bs-info-border-subtle: rgb(233.4, 240.6, 247.8);--bs-warning-border-subtle: rgb(255, 230.2, 155.8);--bs-danger-border-subtle: rgb(241, 174.2, 180.6);--bs-light-border-subtle: #e9ecef;--bs-dark-border-subtle: #adb5bd;--bs-white-rgb: 255, 255, 255;--bs-black-rgb: 0, 0, 0;--bs-font-sans-serif: "Public Sans", Helvetica, Arial, sans-serif;--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, .15), rgba(255, 255, 255, 0));--bs-body-font-family: var(--bs-font-sans-serif);--bs-body-font-size: .875rem;--bs-body-font-weight: 400;--bs-body-line-height: 1.5;--bs-body-color: #212529;--bs-body-color-rgb: 33, 37, 41;--bs-body-bg: #fff;--bs-body-bg-rgb: 255, 255, 255;--bs-emphasis-color: #000;--bs-emphasis-color-rgb: 0, 0, 0;--bs-secondary-color: rgba(33, 37, 41, .75);--bs-secondary-color-rgb: 33, 37, 41;--bs-secondary-bg: #e9ecef;--bs-secondary-bg-rgb: 233, 236, 239;--bs-tertiary-color: rgba(33, 37, 41, .5);--bs-tertiary-color-rgb: 33, 37, 41;--bs-tertiary-bg: #f8f9fa;--bs-tertiary-bg-rgb: 248, 249, 250;--bs-heading-color: inherit;--bs-link-color: #495057;--bs-link-color-rgb: 73, 80, 87;--bs-link-decoration: underline;--bs-link-hover-color: black;--bs-link-hover-color-rgb: 0, 0, 0;--bs-code-color: #d63384;--bs-highlight-color: #212529;--bs-highlight-bg: rgb(255, 242.6, 205.4);--bs-border-width: 1px;--bs-border-style: solid;--bs-border-color: #dee2e6;--bs-border-color-translucent: rgba(0, 0, 0, .175);--bs-border-radius: .375rem;--bs-border-radius-sm: .25rem;--bs-border-radius-lg: .5rem;--bs-border-radius-xl: 1rem;--bs-border-radius-xxl: 2rem;--bs-border-radius-2xl: var(--bs-border-radius-xxl);--bs-border-radius-pill: 50rem;--bs-box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15);--bs-box-shadow-sm: 0 .125rem .25rem rgba(0, 0, 0, .075);--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, .175);--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, .075);--bs-focus-ring-width: .25rem;--bs-focus-ring-opacity: .25;--bs-focus-ring-color: rgba(13, 110, 253, .25);--bs-form-valid-color: #7bd16c;--bs-form-valid-border-color: #7bd16c;--bs-form-invalid-color: #dc3545;--bs-form-invalid-border-color: #dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color: #dee2e6;--bs-body-color-rgb: 222, 226, 230;--bs-body-bg: #212529;--bs-body-bg-rgb: 33, 37, 41;--bs-emphasis-color: #fff;--bs-emphasis-color-rgb: 255, 255, 255;--bs-secondary-color: rgba(222, 226, 230, .75);--bs-secondary-color-rgb: 222, 226, 230;--bs-secondary-bg: #343a40;--bs-secondary-bg-rgb: 52, 58, 64;--bs-tertiary-color: rgba(222, 226, 230, .5);--bs-tertiary-color-rgb: 222, 226, 230;--bs-tertiary-bg: rgb(42.5, 47.5, 52.5);--bs-tertiary-bg-rgb: 43, 48, 53;--bs-primary-text-emphasis: rgb(109.8, 168, 253.8);--bs-secondary-text-emphasis: rgb(166.8, 172.2, 177);--bs-success-text-emphasis: rgb(175.8, 227.4, 166.8);--bs-info-text-emphasis: rgb(222.6, 233.4, 244.2);--bs-warning-text-emphasis: rgb(255, 217.8, 106.2);--bs-danger-text-emphasis: rgb(234, 133.8, 143.4);--bs-light-text-emphasis: #f8f9fa;--bs-dark-text-emphasis: #dee2e6;--bs-primary-bg-subtle: rgb(2.6, 22, 50.6);--bs-secondary-bg-subtle: rgb(21.6, 23.4, 25);--bs-success-bg-subtle: rgb(24.6, 41.8, 21.6);--bs-info-bg-subtle: rgb(40.2, 43.8, 47.4);--bs-warning-bg-subtle: rgb(51, 38.6, 1.4);--bs-danger-bg-subtle: rgb(44, 10.6, 13.8);--bs-light-bg-subtle: #343a40;--bs-dark-bg-subtle: #1a1d20;--bs-primary-border-subtle: rgb(7.8, 66, 151.8);--bs-secondary-border-subtle: rgb(64.8, 70.2, 75);--bs-success-border-subtle: rgb(73.8, 125.4, 64.8);--bs-info-border-subtle: rgb(120.6, 131.4, 142.2);--bs-warning-border-subtle: rgb(153, 115.8, 4.2);--bs-danger-border-subtle: rgb(132, 31.8, 41.4);--bs-light-border-subtle: #495057;--bs-dark-border-subtle: #343a40;--bs-heading-color: inherit;--bs-link-color: rgb(109.8, 168, 253.8);--bs-link-hover-color: rgb(138.84, 185.4, 254.04);--bs-link-color-rgb: 110, 168, 254;--bs-link-hover-color-rgb: 139, 185, 254;--bs-code-color: rgb(230.4, 132.6, 181.2);--bs-highlight-color: #dee2e6;--bs-highlight-bg: rgb(102, 77.2, 2.8);--bs-border-color: #495057;--bs-border-color-translucent: rgba(255, 255, 255, .15);--bs-form-valid-color: rgb(175.8, 227.4, 166.8);--bs-form-valid-border-color: rgb(175.8, 227.4, 166.8);--bs-form-invalid-color: rgb(234, 133.8, 143.4);--bs-form-invalid-border-color: rgb(234, 133.8, 143.4)}*,*:before,*:after{box-sizing:border-box}@media (prefers-reduced-motion: no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}h6,.h6,h5,.h5,h4,.h4,h3,.h3,h2,.h2,h1,.h1{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}h1,.h1{font-size:calc(1.34375rem + 1.125vw)}@media (min-width: 1200px){h1,.h1{font-size:2.1875rem}}h2,.h2{font-size:calc(1.3rem + .6vw)}@media (min-width: 1200px){h2,.h2{font-size:1.75rem}}h3,.h3{font-size:calc(1.278125rem + .3375vw)}@media (min-width: 1200px){h3,.h3{font-size:1.53125rem}}h4,.h4{font-size:calc(1.25625rem + .075vw)}@media (min-width: 1200px){h4,.h4{font-size:1.3125rem}}h5,.h5{font-size:1.09375rem}h6,.h6{font-size:.875rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{text-decoration:underline dotted;cursor:help;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small,.small{font-size:.875em}mark,.mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity, 1));text-decoration:underline}a:hover{--bs-link-color-rgb: var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}thead,tbody,tfoot,tr,td,th{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button:not(:disabled),[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width: 1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-text,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.09375rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width: 1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width: 1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width: 1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width: 1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width: 1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width: 1200px){.display-6{font-size:2.5rem}}.list-unstyled,.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.09375rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer:before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-xxl,.container-xl,.container-lg,.container-md,.container-sm{--bs-gutter-x: 1.5rem;--bs-gutter-y: 0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width: 576px){.container-sm,.container{max-width:540px}}@media (min-width: 768px){.container-md,.container-sm,.container{max-width:720px}}@media (min-width: 992px){.container-lg,.container-md,.container-sm,.container{max-width:960px}}@media (min-width: 1200px){.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1140px}}@media (min-width: 1400px){.container-xxl,.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1320px}}:root{--bs-breakpoint-xs: 0;--bs-breakpoint-sm: 576px;--bs-breakpoint-md: 768px;--bs-breakpoint-lg: 992px;--bs-breakpoint-xl: 1200px;--bs-breakpoint-xxl: 1400px}.row{--bs-gutter-x: 1.5rem;--bs-gutter-y: 0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x: 0}.g-0,.gy-0{--bs-gutter-y: 0}.g-1,.gx-1{--bs-gutter-x: .25rem}.g-1,.gy-1{--bs-gutter-y: .25rem}.g-2,.gx-2{--bs-gutter-x: .5rem}.g-2,.gy-2{--bs-gutter-y: .5rem}.g-3,.gx-3{--bs-gutter-x: 1rem}.g-3,.gy-3{--bs-gutter-y: 1rem}.g-4,.gx-4{--bs-gutter-x: 1.5rem}.g-4,.gy-4{--bs-gutter-y: 1.5rem}.g-5,.gx-5{--bs-gutter-x: 3rem}.g-5,.gy-5{--bs-gutter-y: 3rem}@media (min-width: 576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x: 0}.g-sm-0,.gy-sm-0{--bs-gutter-y: 0}.g-sm-1,.gx-sm-1{--bs-gutter-x: .25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y: .25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x: .5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y: .5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x: 1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y: 1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x: 1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y: 1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x: 3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y: 3rem}}@media (min-width: 768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x: 0}.g-md-0,.gy-md-0{--bs-gutter-y: 0}.g-md-1,.gx-md-1{--bs-gutter-x: .25rem}.g-md-1,.gy-md-1{--bs-gutter-y: .25rem}.g-md-2,.gx-md-2{--bs-gutter-x: .5rem}.g-md-2,.gy-md-2{--bs-gutter-y: .5rem}.g-md-3,.gx-md-3{--bs-gutter-x: 1rem}.g-md-3,.gy-md-3{--bs-gutter-y: 1rem}.g-md-4,.gx-md-4{--bs-gutter-x: 1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y: 1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x: 3rem}.g-md-5,.gy-md-5{--bs-gutter-y: 3rem}}@media (min-width: 992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x: 0}.g-lg-0,.gy-lg-0{--bs-gutter-y: 0}.g-lg-1,.gx-lg-1{--bs-gutter-x: .25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y: .25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x: .5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y: .5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x: 1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y: 1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x: 1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y: 1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x: 3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y: 3rem}}@media (min-width: 1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x: 0}.g-xl-0,.gy-xl-0{--bs-gutter-y: 0}.g-xl-1,.gx-xl-1{--bs-gutter-x: .25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y: .25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x: .5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y: .5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x: 1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y: 1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x: 1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y: 1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x: 3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y: 3rem}}@media (min-width: 1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x: 0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y: 0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x: .25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y: .25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x: .5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y: .5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x: 1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y: 1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x: 1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y: 1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x: 3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y: 3rem}}.table{--bs-table-color-type: initial;--bs-table-bg-type: initial;--bs-table-color-state: initial;--bs-table-bg-state: initial;--bs-table-color: var(--bs-emphasis-color);--bs-table-bg: transparent;--bs-table-border-color: var(--bs-border-color);--bs-table-accent-bg: transparent;--bs-table-striped-color: var(--bs-emphasis-color);--bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), .05);--bs-table-active-color: var(--bs-emphasis-color);--bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), .1);--bs-table-hover-color: var(--bs-emphasis-color);--bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), .075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem;color:var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type: var(--bs-table-striped-color);--bs-table-bg-type: var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type: var(--bs-table-striped-color);--bs-table-bg-type: var(--bs-table-striped-bg)}.table-active{--bs-table-color-state: var(--bs-table-active-color);--bs-table-bg-state: var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state: var(--bs-table-hover-color);--bs-table-bg-state: var(--bs-table-hover-bg)}.table-primary{--bs-table-color: #000;--bs-table-bg: rgb(206.6, 226, 254.6);--bs-table-border-color: rgb(165.28, 180.8, 203.68);--bs-table-striped-bg: rgb(196.27, 214.7, 241.87);--bs-table-striped-color: #000;--bs-table-active-bg: rgb(185.94, 203.4, 229.14);--bs-table-active-color: #000;--bs-table-hover-bg: rgb(191.105, 209.05, 235.505);--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color: #000;--bs-table-bg: rgb(225.6, 227.4, 229);--bs-table-border-color: rgb(180.48, 181.92, 183.2);--bs-table-striped-bg: rgb(214.32, 216.03, 217.55);--bs-table-striped-color: #000;--bs-table-active-bg: rgb(203.04, 204.66, 206.1);--bs-table-active-color: #000;--bs-table-hover-bg: rgb(208.68, 210.345, 211.825);--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color: #000;--bs-table-bg: rgb(228.6, 245.8, 225.6);--bs-table-border-color: rgb(182.88, 196.64, 180.48);--bs-table-striped-bg: rgb(217.17, 233.51, 214.32);--bs-table-striped-color: #000;--bs-table-active-bg: rgb(205.74, 221.22, 203.04);--bs-table-active-color: #000;--bs-table-hover-bg: rgb(211.455, 227.365, 208.68);--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color: #000;--bs-table-bg: rgb(244.2, 247.8, 251.4);--bs-table-border-color: rgb(195.36, 198.24, 201.12);--bs-table-striped-bg: rgb(231.99, 235.41, 238.83);--bs-table-striped-color: #000;--bs-table-active-bg: rgb(219.78, 223.02, 226.26);--bs-table-active-color: #000;--bs-table-hover-bg: rgb(225.885, 229.215, 232.545);--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color: #000;--bs-table-bg: rgb(255, 242.6, 205.4);--bs-table-border-color: rgb(204, 194.08, 164.32);--bs-table-striped-bg: rgb(242.25, 230.47, 195.13);--bs-table-striped-color: #000;--bs-table-active-bg: rgb(229.5, 218.34, 184.86);--bs-table-active-color: #000;--bs-table-hover-bg: rgb(235.875, 224.405, 189.995);--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color: #000;--bs-table-bg: rgb(248, 214.6, 217.8);--bs-table-border-color: rgb(198.4, 171.68, 174.24);--bs-table-striped-bg: rgb(235.6, 203.87, 206.91);--bs-table-striped-color: #000;--bs-table-active-bg: rgb(223.2, 193.14, 196.02);--bs-table-active-color: #000;--bs-table-hover-bg: rgb(229.4, 198.505, 201.465);--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color: #000;--bs-table-bg: #f8f9fa;--bs-table-border-color: rgb(198.4, 199.2, 200);--bs-table-striped-bg: rgb(235.6, 236.55, 237.5);--bs-table-striped-color: #000;--bs-table-active-bg: rgb(223.2, 224.1, 225);--bs-table-active-color: #000;--bs-table-hover-bg: rgb(229.4, 230.325, 231.25);--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color: #fff;--bs-table-bg: #212529;--bs-table-border-color: rgb(77.4, 80.6, 83.8);--bs-table-striped-bg: rgb(44.1, 47.9, 51.7);--bs-table-striped-color: #fff;--bs-table-active-bg: rgb(55.2, 58.8, 62.4);--bs-table-active-color: #fff;--bs-table-hover-bg: rgb(49.65, 53.35, 57.05);--bs-table-hover-color: #fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width: 575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width: 767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width: 991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width: 1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width: 1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.09375rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.765625rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:.875rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem #0d6efd40}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.765625rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.09375rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:.875rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon, none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem #0d6efd40}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.765625rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.09375rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.3125rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg: var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem #0d6efd40}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input[disabled]~.form-check-label,.form-check-input:disabled~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgb%28134, 182.5, 254%29'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check[disabled]+.btn,.btn-check:disabled+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem #0d6efd40}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem #0d6efd40}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-range::-webkit-slider-thumb{transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-range::-moz-range-thumb{transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion: reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control::placeholder,.form-floating>.form-control-plaintext::placeholder{color:transparent}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown),.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill,.form-floating>.form-control-plaintext:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-control-plaintext~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translate(.15rem)}.form-floating>.form-control:focus~label:after,.form-floating>.form-control:not(:placeholder-shown)~label:after,.form-floating>.form-control-plaintext~label:after,.form-floating>.form-select~label:after{position:absolute;top:1rem;right:.375rem;bottom:1rem;left:.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translate(.15rem)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>:disabled~label,.form-floating>.form-control:disabled~label{color:#6c757d}.form-floating>:disabled~label:after,.form-floating>.form-control:disabled~label:after{background-color:var(--bs-secondary-bg)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select,.input-group>.form-floating{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus,.input-group>.form-floating:focus-within{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:.875rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text,.input-group-lg>.btn{padding:.5rem 1rem;font-size:1.09375rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text,.input-group-sm>.btn{padding:.25rem .5rem;font-size:.765625rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select{border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(var(--bs-border-width) * -1);border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.765625rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip,.is-valid~.valid-feedback,.is-valid~.valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%237bd16c' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.was-validated .form-select:valid,.form-select.is-valid{border-color:var(--bs-form-valid-border-color)}.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"],.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"]{--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%237bd16c' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-select:valid:focus,.form-select.is-valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated .form-control-color:valid,.form-control-color.is-valid{width:calc(3.75rem + 1.5em)}.was-validated .form-check-input:valid,.form-check-input.is-valid{border-color:var(--bs-form-valid-border-color)}.was-validated .form-check-input:valid:checked,.form-check-input.is-valid:checked{background-color:var(--bs-form-valid-color)}.was-validated .form-check-input:valid:focus,.form-check-input.is-valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated .form-check-input:valid~.form-check-label,.form-check-input.is-valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):valid,.input-group>.form-control:not(:focus).is-valid,.was-validated .input-group>.form-select:not(:focus):valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.input-group>.form-floating:not(:focus-within).is-valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.765625rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip,.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.was-validated .form-select:invalid,.form-select.is-invalid{border-color:var(--bs-form-invalid-border-color)}.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"],.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-select:invalid:focus,.form-select.is-invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated .form-control-color:invalid,.form-control-color.is-invalid{width:calc(3.75rem + 1.5em)}.was-validated .form-check-input:invalid,.form-check-input.is-invalid{border-color:var(--bs-form-invalid-border-color)}.was-validated .form-check-input:invalid:checked,.form-check-input.is-invalid:checked{background-color:var(--bs-form-invalid-color)}.was-validated .form-check-input:invalid:focus,.form-check-input.is-invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated .form-check-input:invalid~.form-check-label,.form-check-input.is-invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):invalid,.input-group>.form-control:not(:focus).is-invalid,.was-validated .input-group>.form-select:not(:focus):invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.input-group>.form-floating:not(:focus-within).is-invalid{z-index:4}.btn{--bs-btn-padding-x: .75rem;--bs-btn-padding-y: .375rem;--bs-btn-font-family: ;--bs-btn-font-size: .875rem;--bs-btn-font-weight: 400;--bs-btn-line-height: 1.5;--bs-btn-color: var(--bs-body-color);--bs-btn-bg: transparent;--bs-btn-border-width: var(--bs-border-width);--bs-btn-border-color: transparent;--bs-btn-border-radius: .25rem;--bs-btn-hover-border-color: transparent;--bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);--bs-btn-disabled-opacity: .65;--bs-btn-focus-box-shadow: 0 0 0 .25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,:not(.btn-check)+.btn:active,.btn:first-child:active,.btn.active,.btn.show{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,:not(.btn-check)+.btn:active:focus-visible,.btn:first-child:active:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn:disabled,.btn.disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color: #fff;--bs-btn-bg: #0d6efd;--bs-btn-border-color: #0d6efd;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: rgb(11.05, 93.5, 215.05);--bs-btn-hover-border-color: rgb(10.4, 88, 202.4);--bs-btn-focus-shadow-rgb: 49, 132, 253;--bs-btn-active-color: #fff;--bs-btn-active-bg: rgb(10.4, 88, 202.4);--bs-btn-active-border-color: rgb(9.75, 82.5, 189.75);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #0d6efd;--bs-btn-disabled-border-color: #0d6efd}.btn-secondary{--bs-btn-color: #fff;--bs-btn-bg: #6c757d;--bs-btn-border-color: #6c757d;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: rgb(91.8, 99.45, 106.25);--bs-btn-hover-border-color: rgb(86.4, 93.6, 100);--bs-btn-focus-shadow-rgb: 130, 138, 145;--bs-btn-active-color: #fff;--bs-btn-active-bg: rgb(86.4, 93.6, 100);--bs-btn-active-border-color: rgb(81, 87.75, 93.75);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #6c757d;--bs-btn-disabled-border-color: #6c757d}.btn-success{--bs-btn-color: #000;--bs-btn-bg: #7bd16c;--bs-btn-border-color: #7bd16c;--bs-btn-hover-color: #000;--bs-btn-hover-bg: rgb(142.8, 215.9, 130.05);--bs-btn-hover-border-color: rgb(136.2, 213.6, 122.7);--bs-btn-focus-shadow-rgb: 105, 178, 92;--bs-btn-active-color: #000;--bs-btn-active-bg: rgb(149.4, 218.2, 137.4);--bs-btn-active-border-color: rgb(136.2, 213.6, 122.7);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #7bd16c;--bs-btn-disabled-border-color: #7bd16c}.btn-info{--bs-btn-color: #000;--bs-btn-bg: #c9dbed;--bs-btn-border-color: #c9dbed;--bs-btn-hover-color: #000;--bs-btn-hover-bg: rgb(209.1, 224.4, 239.7);--bs-btn-hover-border-color: rgb(206.4, 222.6, 238.8);--bs-btn-focus-shadow-rgb: 171, 186, 201;--bs-btn-active-color: #000;--bs-btn-active-bg: rgb(211.8, 226.2, 240.6);--bs-btn-active-border-color: rgb(206.4, 222.6, 238.8);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #c9dbed;--bs-btn-disabled-border-color: #c9dbed}.btn-warning{--bs-btn-color: #000;--bs-btn-bg: #ffc107;--bs-btn-border-color: #ffc107;--bs-btn-hover-color: #000;--bs-btn-hover-bg: rgb(255, 202.3, 44.2);--bs-btn-hover-border-color: rgb(255, 199.2, 31.8);--bs-btn-focus-shadow-rgb: 217, 164, 6;--bs-btn-active-color: #000;--bs-btn-active-bg: rgb(255, 205.4, 56.6);--bs-btn-active-border-color: rgb(255, 199.2, 31.8);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #ffc107;--bs-btn-disabled-border-color: #ffc107}.btn-danger{--bs-btn-color: #fff;--bs-btn-bg: #dc3545;--bs-btn-border-color: #dc3545;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: rgb(187, 45.05, 58.65);--bs-btn-hover-border-color: rgb(176, 42.4, 55.2);--bs-btn-focus-shadow-rgb: 225, 83, 97;--bs-btn-active-color: #fff;--bs-btn-active-bg: rgb(176, 42.4, 55.2);--bs-btn-active-border-color: rgb(165, 39.75, 51.75);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #dc3545;--bs-btn-disabled-border-color: #dc3545}.btn-light{--bs-btn-color: #000;--bs-btn-bg: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: rgb(210.8, 211.65, 212.5);--bs-btn-hover-border-color: rgb(198.4, 199.2, 200);--bs-btn-focus-shadow-rgb: 211, 212, 213;--bs-btn-active-color: #000;--bs-btn-active-bg: rgb(198.4, 199.2, 200);--bs-btn-active-border-color: rgb(186, 186.75, 187.5);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #f8f9fa;--bs-btn-disabled-border-color: #f8f9fa}.btn-dark{--bs-btn-color: #fff;--bs-btn-bg: #212529;--bs-btn-border-color: #212529;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: rgb(66.3, 69.7, 73.1);--bs-btn-hover-border-color: rgb(55.2, 58.8, 62.4);--bs-btn-focus-shadow-rgb: 66, 70, 73;--bs-btn-active-color: #fff;--bs-btn-active-bg: rgb(77.4, 80.6, 83.8);--bs-btn-active-border-color: rgb(55.2, 58.8, 62.4);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #212529;--bs-btn-disabled-border-color: #212529}.btn-outline-primary{--bs-btn-color: #0d6efd;--bs-btn-border-color: #0d6efd;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #0d6efd;--bs-btn-hover-border-color: #0d6efd;--bs-btn-focus-shadow-rgb: 13, 110, 253;--bs-btn-active-color: #fff;--bs-btn-active-bg: #0d6efd;--bs-btn-active-border-color: #0d6efd;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #0d6efd;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #0d6efd;--bs-gradient: none}.btn-outline-secondary{--bs-btn-color: #6c757d;--bs-btn-border-color: #6c757d;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #6c757d;--bs-btn-hover-border-color: #6c757d;--bs-btn-focus-shadow-rgb: 108, 117, 125;--bs-btn-active-color: #fff;--bs-btn-active-bg: #6c757d;--bs-btn-active-border-color: #6c757d;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #6c757d;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #6c757d;--bs-gradient: none}.btn-outline-success{--bs-btn-color: #7bd16c;--bs-btn-border-color: #7bd16c;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #7bd16c;--bs-btn-hover-border-color: #7bd16c;--bs-btn-focus-shadow-rgb: 123, 209, 108;--bs-btn-active-color: #000;--bs-btn-active-bg: #7bd16c;--bs-btn-active-border-color: #7bd16c;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #7bd16c;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #7bd16c;--bs-gradient: none}.btn-outline-info{--bs-btn-color: #c9dbed;--bs-btn-border-color: #c9dbed;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #c9dbed;--bs-btn-hover-border-color: #c9dbed;--bs-btn-focus-shadow-rgb: 201, 219, 237;--bs-btn-active-color: #000;--bs-btn-active-bg: #c9dbed;--bs-btn-active-border-color: #c9dbed;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #c9dbed;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #c9dbed;--bs-gradient: none}.btn-outline-warning{--bs-btn-color: #ffc107;--bs-btn-border-color: #ffc107;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #ffc107;--bs-btn-hover-border-color: #ffc107;--bs-btn-focus-shadow-rgb: 255, 193, 7;--bs-btn-active-color: #000;--bs-btn-active-bg: #ffc107;--bs-btn-active-border-color: #ffc107;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #ffc107;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #ffc107;--bs-gradient: none}.btn-outline-danger{--bs-btn-color: #dc3545;--bs-btn-border-color: #dc3545;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #dc3545;--bs-btn-hover-border-color: #dc3545;--bs-btn-focus-shadow-rgb: 220, 53, 69;--bs-btn-active-color: #fff;--bs-btn-active-bg: #dc3545;--bs-btn-active-border-color: #dc3545;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #dc3545;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #dc3545;--bs-gradient: none}.btn-outline-light{--bs-btn-color: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #f8f9fa;--bs-btn-hover-border-color: #f8f9fa;--bs-btn-focus-shadow-rgb: 248, 249, 250;--bs-btn-active-color: #000;--bs-btn-active-bg: #f8f9fa;--bs-btn-active-border-color: #f8f9fa;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #f8f9fa;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #f8f9fa;--bs-gradient: none}.btn-outline-dark{--bs-btn-color: #212529;--bs-btn-border-color: #212529;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #212529;--bs-btn-hover-border-color: #212529;--bs-btn-focus-shadow-rgb: 33, 37, 41;--bs-btn-active-color: #fff;--bs-btn-active-bg: #212529;--bs-btn-active-border-color: #212529;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #212529;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #212529;--bs-gradient: none}.btn-link{--bs-btn-font-weight: 400;--bs-btn-color: #495057;--bs-btn-bg: transparent;--bs-btn-border-color: transparent;--bs-btn-hover-color: var(--bs-link-hover-color);--bs-btn-hover-border-color: transparent;--bs-btn-active-color: var(--bs-link-hover-color);--bs-btn-active-border-color: transparent;--bs-btn-disabled-color: #6c757d;--bs-btn-disabled-border-color: transparent;--bs-btn-box-shadow: 0 0 0 #000;--bs-btn-focus-shadow-rgb: 100, 106, 112;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-lg,.btn-group-lg>.btn{--bs-btn-padding-y: .5rem;--bs-btn-padding-x: 1rem;--bs-btn-font-size: 1.09375rem;--bs-btn-border-radius: var(--bs-border-radius-lg)}.btn-sm,.btn-group-sm>.btn{--bs-btn-padding-y: .25rem;--bs-btn-padding-x: .5rem;--bs-btn-font-size: .765625rem;--bs-btn-border-radius: var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion: reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion: reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion: reduce){.collapsing.collapse-horizontal{transition:none}}.dropup,.dropend,.dropdown,.dropstart,.dropup-center,.dropdown-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty:after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex: 1000;--bs-dropdown-min-width: 10rem;--bs-dropdown-padding-x: 0;--bs-dropdown-padding-y: .5rem;--bs-dropdown-spacer: .125rem;--bs-dropdown-font-size: .875rem;--bs-dropdown-color: var(--bs-body-color);--bs-dropdown-bg: var(--bs-body-bg);--bs-dropdown-border-color: var(--bs-border-color-translucent);--bs-dropdown-border-radius: var(--bs-border-radius);--bs-dropdown-border-width: var(--bs-border-width);--bs-dropdown-inner-border-radius: calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg: var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y: .5rem;--bs-dropdown-box-shadow: var(--bs-box-shadow);--bs-dropdown-link-color: var(--bs-body-color);--bs-dropdown-link-hover-color: var(--bs-body-color);--bs-dropdown-link-hover-bg: var(--bs-tertiary-bg);--bs-dropdown-link-active-color: #fff;--bs-dropdown-link-active-bg: #0d6efd;--bs-dropdown-link-disabled-color: var(--bs-tertiary-color);--bs-dropdown-item-padding-x: 1rem;--bs-dropdown-item-padding-y: .25rem;--bs-dropdown-header-color: #6c757d;--bs-dropdown-header-padding-x: 1rem;--bs-dropdown-header-padding-y: .5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position: start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position: end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width: 576px){.dropdown-menu-sm-start{--bs-position: start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position: end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width: 768px){.dropdown-menu-md-start{--bs-position: start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position: end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width: 992px){.dropdown-menu-lg-start{--bs-position: start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position: end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width: 1200px){.dropdown-menu-xl-start{--bs-position: start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position: end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width: 1400px){.dropdown-menu-xxl-start{--bs-position: start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position: end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty:after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty:after{margin-left:0}.dropend .dropdown-toggle:after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle:after{display:none}.dropstart .dropdown-toggle:before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty:after{margin-left:0}.dropstart .dropdown-toggle:before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius, 0)}.dropdown-item:hover,.dropdown-item:focus{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.765625rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color: #dee2e6;--bs-dropdown-bg: #343a40;--bs-dropdown-border-color: var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color: #dee2e6;--bs-dropdown-link-hover-color: #fff;--bs-dropdown-divider-bg: var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg: rgba(255, 255, 255, .15);--bs-dropdown-link-active-color: #fff;--bs-dropdown-link-active-bg: #0d6efd;--bs-dropdown-link-disabled-color: #adb5bd;--bs-dropdown-header-color: #adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;flex:1 1 auto}.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:.25rem}.btn-group>:not(.btn-check:first-child)+.btn,.btn-group>.btn-group:not(:first-child){margin-left:calc(var(--bs-border-width) * -1)}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn,.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split:after,.dropup .dropdown-toggle-split:after,.dropend .dropdown-toggle-split:after{margin-left:0}.dropstart .dropdown-toggle-split:before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:calc(var(--bs-border-width) * -1)}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn~.btn,.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x: 1rem;--bs-nav-link-padding-y: .5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: var(--bs-link-color);--bs-nav-link-hover-color: var(--bs-link-hover-color);--bs-nav-link-disabled-color: var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:none;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion: reduce){.nav-link{transition:none}}.nav-link:hover,.nav-link:focus{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem #0d6efd40}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width: var(--bs-border-width);--bs-nav-tabs-border-color: var(--bs-border-color);--bs-nav-tabs-border-radius: var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color: var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color: var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg: var(--bs-body-bg);--bs-nav-tabs-link-active-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius: var(--bs-border-radius);--bs-nav-pills-link-active-color: #fff;--bs-nav-pills-link-active-bg: #0d6efd}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap: 1rem;--bs-nav-underline-border-width: .125rem;--bs-nav-underline-link-active-color: var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:hover,.nav-underline .nav-link:focus{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill>.nav-link,.nav-fill .nav-item{flex:1 1 auto;text-align:center}.nav-justified>.nav-link,.nav-justified .nav-item{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x: 0;--bs-navbar-padding-y: .5rem;--bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), .65);--bs-navbar-hover-color: rgba(var(--bs-emphasis-color-rgb), .8);--bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), .3);--bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y: .3359375rem;--bs-navbar-brand-margin-end: 1rem;--bs-navbar-brand-font-size: 1.09375rem;--bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x: .5rem;--bs-navbar-toggler-padding-y: .25rem;--bs-navbar-toggler-padding-x: .75rem;--bs-navbar-toggler-font-size: 1.09375rem;--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), .15);--bs-navbar-toggler-border-radius: .25rem;--bs-navbar-toggler-focus-width: .25rem;--bs-navbar-toggler-transition: box-shadow .15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-sm,.navbar>.container-md,.navbar>.container-lg,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x: 0;--bs-nav-link-padding-y: .5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: var(--bs-navbar-color);--bs-nav-link-hover-color: var(--bs-navbar-hover-color);--bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:hover,.navbar-text a:focus{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion: reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height, 75vh);overflow-y:auto}@media (min-width: 576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width: 768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width: 992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width: 1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width: 1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color: rgba(255, 255, 255, .55);--bs-navbar-hover-color: rgba(255, 255, 255, .75);--bs-navbar-disabled-color: rgba(255, 255, 255, .25);--bs-navbar-active-color: #fff;--bs-navbar-brand-color: #fff;--bs-navbar-brand-hover-color: #fff;--bs-navbar-toggler-border-color: rgba(255, 255, 255, .1);--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y: 1rem;--bs-card-spacer-x: 1rem;--bs-card-title-spacer-y: .5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width: var(--bs-border-width);--bs-card-border-color: var(--bs-border-color-translucent);--bs-card-border-radius: var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y: .5rem;--bs-card-cap-padding-x: 1rem;--bs-card-cap-bg: rgba(var(--bs-body-color-rgb), .03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg: var(--bs-body-bg);--bs-card-img-overlay-padding: 1rem;--bs-card-group-margin: .75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-top,.card-img-bottom{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width: 576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-img-top,.card-group>.card:not(:last-child) .card-header{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-img-bottom,.card-group>.card:not(:last-child) .card-footer{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-img-top,.card-group>.card:not(:first-child) .card-header{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-img-bottom,.card-group>.card:not(:first-child) .card-footer{border-bottom-left-radius:0}}.accordion{--bs-accordion-color: var(--bs-body-color);--bs-accordion-bg: var(--bs-body-bg);--bs-accordion-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out, border-radius .15s ease;--bs-accordion-border-color: var(--bs-border-color);--bs-accordion-border-width: var(--bs-border-width);--bs-accordion-border-radius: var(--bs-border-radius);--bs-accordion-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x: 1.25rem;--bs-accordion-btn-padding-y: 1rem;--bs-accordion-btn-color: var(--bs-body-color);--bs-accordion-btn-bg: var(--bs-accordion-bg);--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width: 1.25rem;--bs-accordion-btn-icon-transform: rotate(-180deg);--bs-accordion-btn-icon-transition: transform .2s ease-in-out;--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='rgb%285.2, 44, 101.2%29' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow: 0 0 0 .25rem rgba(13, 110, 253, .25);--bs-accordion-body-padding-x: 1.25rem;--bs-accordion-body-padding-y: 1rem;--bs-accordion-active-color: var(--bs-primary-text-emphasis);--bs-accordion-active-bg: var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:.875rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion: reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed):after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button:after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion: reduce){.accordion-button:after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}.accordion-flush>.accordion-item>.accordion-collapse{border-radius:0}[data-bs-theme=dark] .accordion-button:after{--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='rgb%28109.8, 168, 253.8%29'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='rgb%28109.8, 168, 253.8%29'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x: 0;--bs-breadcrumb-padding-y: 0;--bs-breadcrumb-margin-bottom: 1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color: var(--bs-secondary-color);--bs-breadcrumb-item-padding-x: .5rem;--bs-breadcrumb-item-active-color: var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item:before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x: .75rem;--bs-pagination-padding-y: .375rem;--bs-pagination-font-size: .875rem;--bs-pagination-color: var(--bs-link-color);--bs-pagination-bg: var(--bs-body-bg);--bs-pagination-border-width: var(--bs-border-width);--bs-pagination-border-color: var(--bs-border-color);--bs-pagination-border-radius: var(--bs-border-radius);--bs-pagination-hover-color: var(--bs-link-hover-color);--bs-pagination-hover-bg: var(--bs-tertiary-bg);--bs-pagination-hover-border-color: var(--bs-border-color);--bs-pagination-focus-color: var(--bs-link-hover-color);--bs-pagination-focus-bg: var(--bs-secondary-bg);--bs-pagination-focus-box-shadow: 0 0 0 .25rem rgba(13, 110, 253, .25);--bs-pagination-active-color: #fff;--bs-pagination-active-bg: #0d6efd;--bs-pagination-active-border-color: #0d6efd;--bs-pagination-disabled-color: var(--bs-secondary-color);--bs-pagination-disabled-bg: var(--bs-secondary-bg);--bs-pagination-disabled-border-color: var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion: reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.page-link.active,.active>.page-link{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.page-link.disabled,.disabled>.page-link{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width) * -1)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x: 1.5rem;--bs-pagination-padding-y: .75rem;--bs-pagination-font-size: 1.09375rem;--bs-pagination-border-radius: var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x: .5rem;--bs-pagination-padding-y: .25rem;--bs-pagination-font-size: .765625rem;--bs-pagination-border-radius: var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x: .65em;--bs-badge-padding-y: .35em;--bs-badge-font-size: .75em;--bs-badge-font-weight: 700;--bs-badge-color: #fff;--bs-badge-border-radius: var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg: transparent;--bs-alert-padding-x: 1rem;--bs-alert-padding-y: 1rem;--bs-alert-margin-bottom: 1rem;--bs-alert-color: inherit;--bs-alert-border-color: transparent;--bs-alert-border: var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius: var(--bs-border-radius);--bs-alert-link-color: inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color: var(--bs-primary-text-emphasis);--bs-alert-bg: var(--bs-primary-bg-subtle);--bs-alert-border-color: var(--bs-primary-border-subtle);--bs-alert-link-color: var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color: var(--bs-secondary-text-emphasis);--bs-alert-bg: var(--bs-secondary-bg-subtle);--bs-alert-border-color: var(--bs-secondary-border-subtle);--bs-alert-link-color: var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color: var(--bs-success-text-emphasis);--bs-alert-bg: var(--bs-success-bg-subtle);--bs-alert-border-color: var(--bs-success-border-subtle);--bs-alert-link-color: var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color: var(--bs-info-text-emphasis);--bs-alert-bg: var(--bs-info-bg-subtle);--bs-alert-border-color: var(--bs-info-border-subtle);--bs-alert-link-color: var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color: var(--bs-warning-text-emphasis);--bs-alert-bg: var(--bs-warning-bg-subtle);--bs-alert-border-color: var(--bs-warning-border-subtle);--bs-alert-link-color: var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color: var(--bs-danger-text-emphasis);--bs-alert-bg: var(--bs-danger-bg-subtle);--bs-alert-border-color: var(--bs-danger-border-subtle);--bs-alert-link-color: var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color: var(--bs-light-text-emphasis);--bs-alert-bg: var(--bs-light-bg-subtle);--bs-alert-border-color: var(--bs-light-border-subtle);--bs-alert-link-color: var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color: var(--bs-dark-text-emphasis);--bs-alert-bg: var(--bs-dark-bg-subtle);--bs-alert-border-color: var(--bs-dark-border-subtle);--bs-alert-link-color: var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress,.progress-stacked{--bs-progress-height: 1rem;--bs-progress-font-size: .65625rem;--bs-progress-bg: var(--bs-secondary-bg);--bs-progress-border-radius: var(--bs-border-radius);--bs-progress-box-shadow: var(--bs-box-shadow-inset);--bs-progress-bar-color: #fff;--bs-progress-bar-bg: #0d6efd;--bs-progress-bar-transition: width .6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion: reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion: reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color: var(--bs-body-color);--bs-list-group-bg: var(--bs-body-bg);--bs-list-group-border-color: var(--bs-border-color);--bs-list-group-border-width: var(--bs-border-width);--bs-list-group-border-radius: var(--bs-border-radius);--bs-list-group-item-padding-x: 1rem;--bs-list-group-item-padding-y: .5rem;--bs-list-group-action-color: var(--bs-secondary-color);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-tertiary-bg);--bs-list-group-action-active-color: var(--bs-body-color);--bs-list-group-action-active-bg: var(--bs-secondary-bg);--bs-list-group-disabled-color: var(--bs-secondary-color);--bs-list-group-disabled-bg: var(--bs-body-bg);--bs-list-group-active-color: #fff;--bs-list-group-active-bg: #0d6efd;--bs-list-group-active-border-color: #0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item:before{content:counters(section,".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width: 576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width: 768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width: 992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width: 1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width: 1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color: var(--bs-primary-text-emphasis);--bs-list-group-bg: var(--bs-primary-bg-subtle);--bs-list-group-border-color: var(--bs-primary-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-primary-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-primary-border-subtle);--bs-list-group-active-color: var(--bs-primary-bg-subtle);--bs-list-group-active-bg: var(--bs-primary-text-emphasis);--bs-list-group-active-border-color: var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color: var(--bs-secondary-text-emphasis);--bs-list-group-bg: var(--bs-secondary-bg-subtle);--bs-list-group-border-color: var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-secondary-border-subtle);--bs-list-group-active-color: var(--bs-secondary-bg-subtle);--bs-list-group-active-bg: var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color: var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color: var(--bs-success-text-emphasis);--bs-list-group-bg: var(--bs-success-bg-subtle);--bs-list-group-border-color: var(--bs-success-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-success-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-success-border-subtle);--bs-list-group-active-color: var(--bs-success-bg-subtle);--bs-list-group-active-bg: var(--bs-success-text-emphasis);--bs-list-group-active-border-color: var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color: var(--bs-info-text-emphasis);--bs-list-group-bg: var(--bs-info-bg-subtle);--bs-list-group-border-color: var(--bs-info-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-info-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-info-border-subtle);--bs-list-group-active-color: var(--bs-info-bg-subtle);--bs-list-group-active-bg: var(--bs-info-text-emphasis);--bs-list-group-active-border-color: var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color: var(--bs-warning-text-emphasis);--bs-list-group-bg: var(--bs-warning-bg-subtle);--bs-list-group-border-color: var(--bs-warning-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-warning-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-warning-border-subtle);--bs-list-group-active-color: var(--bs-warning-bg-subtle);--bs-list-group-active-bg: var(--bs-warning-text-emphasis);--bs-list-group-active-border-color: var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color: var(--bs-danger-text-emphasis);--bs-list-group-bg: var(--bs-danger-bg-subtle);--bs-list-group-border-color: var(--bs-danger-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-danger-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-danger-border-subtle);--bs-list-group-active-color: var(--bs-danger-bg-subtle);--bs-list-group-active-bg: var(--bs-danger-text-emphasis);--bs-list-group-active-border-color: var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color: var(--bs-light-text-emphasis);--bs-list-group-bg: var(--bs-light-bg-subtle);--bs-list-group-border-color: var(--bs-light-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-light-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-light-border-subtle);--bs-list-group-active-color: var(--bs-light-bg-subtle);--bs-list-group-active-bg: var(--bs-light-text-emphasis);--bs-list-group-active-border-color: var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color: var(--bs-dark-text-emphasis);--bs-list-group-bg: var(--bs-dark-bg-subtle);--bs-list-group-border-color: var(--bs-dark-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-dark-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-dark-border-subtle);--bs-list-group-active-color: var(--bs-dark-bg-subtle);--bs-list-group-active-bg: var(--bs-dark-text-emphasis);--bs-list-group-active-border-color: var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color: #000;--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity: .5;--bs-btn-close-hover-opacity: .75;--bs-btn-close-focus-shadow: 0 0 0 .25rem rgba(13, 110, 253, .25);--bs-btn-close-focus-opacity: 1;--bs-btn-close-disabled-opacity: .25;--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close:disabled,.btn-close.disabled{pointer-events:none;-webkit-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white,[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex: 1090;--bs-toast-padding-x: .75rem;--bs-toast-padding-y: .5rem;--bs-toast-spacing: 1.5rem;--bs-toast-max-width: 380px;--bs-toast-font-size: .875rem;--bs-toast-color: ;--bs-toast-bg: rgba(var(--bs-body-bg-rgb), .85);--bs-toast-border-width: var(--bs-border-width);--bs-toast-border-color: var(--bs-border-color-translucent);--bs-toast-border-radius: var(--bs-border-radius);--bs-toast-box-shadow: var(--bs-box-shadow);--bs-toast-header-color: var(--bs-secondary-color);--bs-toast-header-bg: rgba(var(--bs-body-bg-rgb), .85);--bs-toast-header-border-color: var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex: 1090;position:absolute;z-index:var(--bs-toast-zindex);width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex: 1055;--bs-modal-width: 500px;--bs-modal-padding: 1rem;--bs-modal-margin: .5rem;--bs-modal-color: ;--bs-modal-bg: var(--bs-body-bg);--bs-modal-border-color: var(--bs-border-color-translucent);--bs-modal-border-width: var(--bs-border-width);--bs-modal-border-radius: var(--bs-border-radius-lg);--bs-modal-box-shadow: var(--bs-box-shadow-sm);--bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x: 1rem;--bs-modal-header-padding-y: 1rem;--bs-modal-header-padding: 1rem 1rem;--bs-modal-header-border-color: var(--bs-border-color);--bs-modal-header-border-width: var(--bs-border-width);--bs-modal-title-line-height: 1.5;--bs-modal-footer-gap: .5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color: var(--bs-border-color);--bs-modal-footer-border-width: var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translateY(-50px)}@media (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex: 1050;--bs-backdrop-bg: #000;--bs-backdrop-opacity: .5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width: 576px){.modal{--bs-modal-margin: 1.75rem;--bs-modal-box-shadow: var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width: 300px}}@media (min-width: 992px){.modal-lg,.modal-xl{--bs-modal-width: 800px}}@media (min-width: 1200px){.modal-xl{--bs-modal-width: 1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header,.modal-fullscreen .modal-footer{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width: 575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header,.modal-fullscreen-sm-down .modal-footer{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width: 767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header,.modal-fullscreen-md-down .modal-footer{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width: 991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header,.modal-fullscreen-lg-down .modal-footer{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width: 1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header,.modal-fullscreen-xl-down .modal-footer{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width: 1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header,.modal-fullscreen-xxl-down .modal-footer{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex: 1080;--bs-tooltip-max-width: 200px;--bs-tooltip-padding-x: .5rem;--bs-tooltip-padding-y: .25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size: .765625rem;--bs-tooltip-color: var(--bs-body-bg);--bs-tooltip-bg: var(--bs-emphasis-color);--bs-tooltip-border-radius: var(--bs-border-radius);--bs-tooltip-opacity: .9;--bs-tooltip-arrow-width: .8rem;--bs-tooltip-arrow-height: .4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow:before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-top .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-top .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow:before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-end .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-end .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow:before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-bottom .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-bottom .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow:before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-start .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-start .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow:before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex: 1070;--bs-popover-max-width: 276px;--bs-popover-font-size: .765625rem;--bs-popover-bg: var(--bs-body-bg);--bs-popover-border-width: var(--bs-border-width);--bs-popover-border-color: var(--bs-border-color-translucent);--bs-popover-border-radius: var(--bs-border-radius-lg);--bs-popover-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow: var(--bs-box-shadow);--bs-popover-header-padding-x: 1rem;--bs-popover-header-padding-y: .5rem;--bs-popover-header-font-size: .875rem;--bs-popover-header-color: inherit;--bs-popover-header-bg: var(--bs-secondary-bg);--bs-popover-body-padding-x: 1rem;--bs-popover-body-padding-y: 1rem;--bs-popover-body-color: var(--bs-body-color);--bs-popover-arrow-width: 1rem;--bs-popover-arrow-height: .5rem;--bs-popover-arrow-border: var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow:before,.popover .popover-arrow:after{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-top>.popover-arrow,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-top>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:before,.bs-popover-top>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:after{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-top>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-top>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-end>.popover-arrow,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-end>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:before,.bs-popover-end>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:after{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-end>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-end>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-bottom>.popover-arrow,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-bottom>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:before,.bs-popover-bottom>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:after{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-bottom>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-bottom>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-bottom .popover-header:before,.bs-popover-auto[data-popper-placement^=bottom] .popover-header:before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-start>.popover-arrow,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-start>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:before,.bs-popover-start>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:after{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-start>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-start>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner:after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion: reduce){.carousel-item{transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-start),.active.carousel-item-end{transform:translate(100%)}.carousel-item-prev:not(.carousel-item-end),.active.carousel-item-start{transform:translate(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end{z-index:1;opacity:1}.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{transition:none}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:none;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion: reduce){.carousel-control-prev,.carousel-control-next{transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion: reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-prev-icon,.carousel-dark .carousel-control-next-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-grow,.spinner-border{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width: 2rem;--bs-spinner-height: 2rem;--bs-spinner-vertical-align: -.125em;--bs-spinner-border-width: .25em;--bs-spinner-animation-speed: .75s;--bs-spinner-animation-name: spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width: 1rem;--bs-spinner-height: 1rem;--bs-spinner-border-width: .2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width: 2rem;--bs-spinner-height: 2rem;--bs-spinner-vertical-align: -.125em;--bs-spinner-animation-speed: .75s;--bs-spinner-animation-name: spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width: 1rem;--bs-spinner-height: 1rem}@media (prefers-reduced-motion: reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed: 1.5s}}.offcanvas,.offcanvas-xxl,.offcanvas-xl,.offcanvas-lg,.offcanvas-md,.offcanvas-sm{--bs-offcanvas-zindex: 1045;--bs-offcanvas-width: 400px;--bs-offcanvas-height: 30vh;--bs-offcanvas-padding-x: 1rem;--bs-offcanvas-padding-y: 1rem;--bs-offcanvas-color: var(--bs-body-color);--bs-offcanvas-bg: var(--bs-body-bg);--bs-offcanvas-border-width: var(--bs-border-width);--bs-offcanvas-border-color: var(--bs-border-color-translucent);--bs-offcanvas-box-shadow: var(--bs-box-shadow-sm);--bs-offcanvas-transition: transform .3s ease-in-out;--bs-offcanvas-title-line-height: 1.5}@media (max-width: 575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width: 575.98px) and (prefers-reduced-motion: reduce){.offcanvas-sm{transition:none}}@media (max-width: 575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.showing,.offcanvas-sm.show:not(.hiding){transform:none}.offcanvas-sm.showing,.offcanvas-sm.hiding,.offcanvas-sm.show{visibility:visible}}@media (min-width: 576px){.offcanvas-sm{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width: 767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width: 767.98px) and (prefers-reduced-motion: reduce){.offcanvas-md{transition:none}}@media (max-width: 767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.showing,.offcanvas-md.show:not(.hiding){transform:none}.offcanvas-md.showing,.offcanvas-md.hiding,.offcanvas-md.show{visibility:visible}}@media (min-width: 768px){.offcanvas-md{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width: 991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width: 991.98px) and (prefers-reduced-motion: reduce){.offcanvas-lg{transition:none}}@media (max-width: 991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.showing,.offcanvas-lg.show:not(.hiding){transform:none}.offcanvas-lg.showing,.offcanvas-lg.hiding,.offcanvas-lg.show{visibility:visible}}@media (min-width: 992px){.offcanvas-lg{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width: 1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width: 1199.98px) and (prefers-reduced-motion: reduce){.offcanvas-xl{transition:none}}@media (max-width: 1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.showing,.offcanvas-xl.show:not(.hiding){transform:none}.offcanvas-xl.showing,.offcanvas-xl.hiding,.offcanvas-xl.show{visibility:visible}}@media (min-width: 1200px){.offcanvas-xl{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width: 1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width: 1399.98px) and (prefers-reduced-motion: reduce){.offcanvas-xxl{transition:none}}@media (max-width: 1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.showing,.offcanvas-xxl.show:not(.hiding){transform:none}.offcanvas-xxl.showing,.offcanvas-xxl.hiding,.offcanvas-xxl.show{visibility:visible}}@media (min-width: 1400px){.offcanvas-xxl{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion: reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translate(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.showing,.offcanvas.show:not(.hiding){transform:none}.offcanvas.showing,.offcanvas.hiding,.offcanvas.show{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin:calc(-.5 * var(--bs-offcanvas-padding-y)) calc(-.5 * var(--bs-offcanvas-padding-x)) calc(-.5 * var(--bs-offcanvas-padding-y)) auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn:before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,#000c,#000 95%);mask-image:linear-gradient(130deg,#000 55%,#000c,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{to{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix:after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity, 1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity, 1))!important}.text-bg-success{color:#000!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity, 1))!important}.text-bg-info{color:#000!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity, 1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity, 1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity, 1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity, 1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity, 1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity, 1))!important}.link-primary:hover,.link-primary:focus{color:RGBA(10,88,202,var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity, 1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity, 1))!important}.link-secondary:hover,.link-secondary:focus{color:RGBA(86,94,100,var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity, 1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity, 1))!important}.link-success:hover,.link-success:focus{color:RGBA(149,218,137,var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(149,218,137,var(--bs-link-underline-opacity, 1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity, 1))!important}.link-info:hover,.link-info:focus{color:RGBA(212,226,241,var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(212,226,241,var(--bs-link-underline-opacity, 1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity, 1))!important}.link-warning:hover,.link-warning:focus{color:RGBA(255,205,57,var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity, 1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity, 1))!important}.link-danger:hover,.link-danger:focus{color:RGBA(176,42,55,var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity, 1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity, 1))!important}.link-light:hover,.link-light:focus{color:RGBA(249,250,251,var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity, 1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity, 1))!important}.link-dark:hover,.link-dark:focus{color:RGBA(26,30,33,var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity, 1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity, 1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity, 1))!important}.link-body-emphasis:hover,.link-body-emphasis:focus{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity, .75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity, .75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity, .5));text-underline-offset:.25em;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion: reduce){.icon-link>.bi{transition:none}}.icon-link-hover:hover>.bi,.icon-link-hover:focus-visible>.bi{transform:var(--bs-icon-link-transform, translate3d(.25em, 0, 0))}.ratio{position:relative;width:100%}.ratio:before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio: 100%}.ratio-4x3{--bs-aspect-ratio: 75%}.ratio-16x9{--bs-aspect-ratio: 56.25%}.ratio-21x9{--bs-aspect-ratio: 42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:sticky;top:0;z-index:1020}.sticky-bottom{position:sticky;bottom:0;z-index:1020}@media (min-width: 576px){.sticky-sm-top{position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width: 768px){.sticky-md-top{position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width: 992px){.sticky-lg-top{position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width: 1200px){.sticky-xl-top{position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width: 1400px){.sticky-xxl-top{position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden:not(caption),.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption){position:absolute!important}.stretched-link:after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{object-fit:contain!important}.object-fit-cover{object-fit:cover!important}.object-fit-fill{object-fit:fill!important}.object-fit-scale{object-fit:scale-down!important}.object-fit-none{object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color: rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color: rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color: rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color: rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color: rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color: rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color: rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color: rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translate(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity: 1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity: 1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity: 1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity: 1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity: 1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity: 1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity: 1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity: 1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity: 1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity: 1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity: .1}.border-opacity-25{--bs-border-opacity: .25}.border-opacity-50{--bs-border-opacity: .5}.border-opacity-75{--bs-border-opacity: .75}.border-opacity-100{--bs-border-opacity: 1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{column-gap:0!important}.column-gap-1{column-gap:.25rem!important}.column-gap-2{column-gap:.5rem!important}.column-gap-3{column-gap:1rem!important}.column-gap-4{column-gap:1.5rem!important}.column-gap-5{column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.34375rem + 1.125vw)!important}.fs-2{font-size:calc(1.3rem + .6vw)!important}.fs-3{font-size:calc(1.278125rem + .3375vw)!important}.fs-4{font-size:calc(1.25625rem + .075vw)!important}.fs-5{font-size:1.09375rem!important}.fs-6{font-size:.875rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity: 1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity: 1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity: 1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity: 1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity: 1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity: 1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity: 1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity: 1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity: 1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity: 1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity: 1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity: 1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity: 1;color:#00000080!important}.text-white-50{--bs-text-opacity: 1;color:#ffffff80!important}.text-body-secondary{--bs-text-opacity: 1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity: 1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity: 1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity: 1;color:inherit!important}.text-opacity-25{--bs-text-opacity: .25}.text-opacity-50{--bs-text-opacity: .5}.text-opacity-75{--bs-text-opacity: .75}.text-opacity-100{--bs-text-opacity: 1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10,.link-opacity-10-hover:hover{--bs-link-opacity: .1}.link-opacity-25,.link-opacity-25-hover:hover{--bs-link-opacity: .25}.link-opacity-50,.link-opacity-50-hover:hover{--bs-link-opacity: .5}.link-opacity-75,.link-opacity-75-hover:hover{--bs-link-opacity: .75}.link-opacity-100,.link-opacity-100-hover:hover{--bs-link-opacity: 1}.link-offset-1,.link-offset-1-hover:hover{text-underline-offset:.125em!important}.link-offset-2,.link-offset-2-hover:hover{text-underline-offset:.25em!important}.link-offset-3,.link-offset-3-hover:hover{text-underline-offset:.375em!important}.link-underline-primary{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity, 1))!important}.link-underline-opacity-0,.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity: 0}.link-underline-opacity-10,.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity: .1}.link-underline-opacity-25,.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity: .25}.link-underline-opacity-50,.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity: .5}.link-underline-opacity-75,.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity: .75}.link-underline-opacity-100,.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity: 1}.bg-primary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity: 1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity: 1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity: 1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity: 1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity: 1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity: 1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity: 1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity: 1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity: 1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity: 1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity: .1}.bg-opacity-25{--bs-bg-opacity: .25}.bg-opacity-50{--bs-bg-opacity: .5}.bg-opacity-75{--bs-bg-opacity: .75}.bg-opacity-100{--bs-bg-opacity: 1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width: 576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{object-fit:contain!important}.object-fit-sm-cover{object-fit:cover!important}.object-fit-sm-fill{object-fit:fill!important}.object-fit-sm-scale{object-fit:scale-down!important}.object-fit-sm-none{object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{column-gap:0!important}.column-gap-sm-1{column-gap:.25rem!important}.column-gap-sm-2{column-gap:.5rem!important}.column-gap-sm-3{column-gap:1rem!important}.column-gap-sm-4{column-gap:1.5rem!important}.column-gap-sm-5{column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width: 768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{object-fit:contain!important}.object-fit-md-cover{object-fit:cover!important}.object-fit-md-fill{object-fit:fill!important}.object-fit-md-scale{object-fit:scale-down!important}.object-fit-md-none{object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{column-gap:0!important}.column-gap-md-1{column-gap:.25rem!important}.column-gap-md-2{column-gap:.5rem!important}.column-gap-md-3{column-gap:1rem!important}.column-gap-md-4{column-gap:1.5rem!important}.column-gap-md-5{column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width: 992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{object-fit:contain!important}.object-fit-lg-cover{object-fit:cover!important}.object-fit-lg-fill{object-fit:fill!important}.object-fit-lg-scale{object-fit:scale-down!important}.object-fit-lg-none{object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{column-gap:0!important}.column-gap-lg-1{column-gap:.25rem!important}.column-gap-lg-2{column-gap:.5rem!important}.column-gap-lg-3{column-gap:1rem!important}.column-gap-lg-4{column-gap:1.5rem!important}.column-gap-lg-5{column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width: 1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{object-fit:contain!important}.object-fit-xl-cover{object-fit:cover!important}.object-fit-xl-fill{object-fit:fill!important}.object-fit-xl-scale{object-fit:scale-down!important}.object-fit-xl-none{object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{column-gap:0!important}.column-gap-xl-1{column-gap:.25rem!important}.column-gap-xl-2{column-gap:.5rem!important}.column-gap-xl-3{column-gap:1rem!important}.column-gap-xl-4{column-gap:1.5rem!important}.column-gap-xl-5{column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width: 1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{object-fit:contain!important}.object-fit-xxl-cover{object-fit:cover!important}.object-fit-xxl-fill{object-fit:fill!important}.object-fit-xxl-scale{object-fit:scale-down!important}.object-fit-xxl-none{object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{column-gap:0!important}.column-gap-xxl-1{column-gap:.25rem!important}.column-gap-xxl-2{column-gap:.5rem!important}.column-gap-xxl-3{column-gap:1rem!important}.column-gap-xxl-4{column-gap:1.5rem!important}.column-gap-xxl-5{column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width: 1200px){.fs-1{font-size:2.1875rem!important}.fs-2{font-size:1.75rem!important}.fs-3{font-size:1.53125rem!important}.fs-4{font-size:1.3125rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}h1,.h1,h2,.h2,h3,.h3,h4,.h4,h5,.h5,h6,.h6{font-family:Sanchez,sans-serif}hr{margin:2em 0}.text-ellipsis{text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.circle,.disc{display:inline-block;height:1em;width:1em;border-radius:1em;vertical-align:top}.circle{position:relative}.circle:after{content:" ";position:absolute;top:1px;right:1px;bottom:1px;left:1px;border-radius:1em;background:#fff}.hoverable:hover{cursor:pointer;opacity:.7}.triangle{width:0;height:0;border-top:.3em solid transparent;border-bottom:.3em solid transparent}.triangle-left{border-right:.8em solid #ccc}.triangle-right{border-left:.8em solid #ccc}.line{width:2.6em;height:.25em;background:#ccc;position:relative}.line:first-child:last-child{width:3.4em}.line .count{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);padding:.3em;background:#fff;box-sizing:content-box}.edge,.edges{display:inline-block;width:3em;text-align:center}.btn>svg{vertical-align:text-bottom}.btn-inline{line-height:1em}.w-1{width:1%!important}.w-45{width:45%!important}.line-height-1{line-height:1em!important}.flex-regular-width{flex-shrink:1!important;flex-grow:1!important;flex-basis:0!important;width:0!important}input:placeholder-shown{text-overflow:ellipsis;overflow:hidden}.cursor-pointer{cursor:pointer!important}.input-inline{width:5em}.custom-scrollbar,.modal .modal-body,.side-panel .panel-content,.react-select__menu-list{scrollbar-color:grey whitesmoke;scrollbar-width:thin}.custom-scrollbar::-webkit-scrollbar-track,.modal .modal-body::-webkit-scrollbar-track,.side-panel .panel-content::-webkit-scrollbar-track,.react-select__menu-list::-webkit-scrollbar-track{background-color:#f5f5f5}.custom-scrollbar::-webkit-scrollbar,.modal .modal-body::-webkit-scrollbar,.side-panel .panel-content::-webkit-scrollbar,.react-select__menu-list::-webkit-scrollbar{width:3px;height:3px;background-color:#fff}.custom-scrollbar::-webkit-scrollbar-thumb,.modal .modal-body::-webkit-scrollbar-thumb,.side-panel .panel-content::-webkit-scrollbar-thumb,.react-select__menu-list::-webkit-scrollbar-thumb{background-color:gray}input[type=number]{-moz-appearance:textfield}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none}.react-select__menu-portal{z-index:1080!important}.react-select__control{border-color:#000!important}.react-select__menu{margin-bottom:1em}.react-select__control--is-focused{box-shadow:0 0 0 1px #000!important}.scrollbar-left{direction:rtl}.scrollbar-left>*{direction:ltr}.dropzone{padding:4rem;border-radius:1rem;border:3px dotted black;width:100%}.flex-centered{display:flex;align-items:center;justify-content:center}.fill{position:absolute;top:0;right:0;bottom:0;left:0}.hidden{visibility:hidden}.with-end-buttons{display:flex;flex-direction:row;align-items:center}.with-end-buttons>*:first-child{flex-grow:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;line-height:1.8em}.with-end-buttons>*:not(:first-child){flex-shrink:0}.ellipsis,.range-filter .label{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.box{display:inline-block;width:1.5em;text-align:center}body{padding:0;margin:0}body #root{display:flex;flex-direction:row;background:#fff;align-items:stretch;overflow:hidden;width:100vw;height:100vh}body #root main{flex-grow:1}body #toasts-container{position:fixed;bottom:0;right:0;z-index:1080}body #toasts-container .toast{max-width:calc(100vw - 1rem)}body #portal-target{position:absolute;top:0;left:0}.side-panel{border-right:1px solid #dee2e6;overflow:hidden;height:100%;z-index:102;display:flex;flex-direction:column}.side-panel .block:not(:last-child){border-bottom:1px solid #dee2e6}.side-panel .panel-header{border-bottom:1px solid #dee2e6;flex-shrink:0}.side-panel .panel-header .header-buttons{padding-left:4.5em!important}.side-panel .panel-content{flex-shrink:1;flex-grow:1;flex-basis:0;display:flex;flex-direction:column;overflow-y:scroll}.side-panel .panel-content>*>*:not(hr){padding:1rem}.graph-view{height:100%;position:relative;overflow:hidden}.graph-view .wrapper{position:absolute;inset:0 0 0 auto;display:flex;flex-direction:row;transition:width ease-in-out .5s}.edition-panel{transition:all ease-in-out .5s;z-index:103}.edition-panel.expanded{box-shadow:5px 0 15px #00000059}@media (min-width: 768px){.graph-view.panel-collapsed .wrapper{width:calc(500px + 100%)}.graph-view.panel-expanded .wrapper{width:100%}.side-panel{width:500px}.edition-panel.collapsed{margin-left:-500px}}@media (max-width: 767.98px){.graph-view.panel-collapsed .wrapper{width:200%}.graph-view.panel-expanded .wrapper{width:100%}.side-panel{width:100vw}.edition-panel.collapsed{margin-left:-100vw}}.modal{display:block}.modal .modal-backdrop{opacity:.1;z-index:1050}.modal .modal-content{z-index:1055}.home-view{display:flex;flex-direction:column;min-height:100vh}.home-view .expanding-block{width:100%}.home-view .title-block{flex-grow:1;padding:20vh .5rem .5rem;max-width:500px;margin:0 auto;text-align:center;min-height:85vh}.home-view .gexf-form{transition:opacity ease-in-out .6s}.home-view .footer{flex-shrink:0;width:600px;max-width:100%;margin:0 auto}.graph-view .graph-button{width:2em;height:2em;text-align:center;display:flex;align-items:center;justify-content:center;font-size:1.5em;margin-bottom:.4em;background:#fff}.graph-view .graph-button:hover{background:#000}.graph-view .graph{position:relative;flex-grow:1}.graph-view .graph .controls{position:absolute;top:1rem;right:1rem;display:flex;flex-direction:column;align-items:flex-end}.graph-view .graph .controls>*{z-index:103}.graph-view .graph .captions{position:absolute;bottom:1rem;left:1rem}.graph-view .graph .captions .size-caption{z-index:100}.graph-view .graph .captions .size-caption .nodes{display:flex;flex-direction:row;align-items:flex-end}.graph-view .graph .captions .size-caption .circle-wrapper{height:50px;overflow:hidden;display:flex;align-items:center;min-width:30px;justify-content:center}.graph-view .graph .captions .size-caption .dotted-circle{border-radius:100%;background:#ccc6;border:2px dotted black}.graph-view .graph .sigma-wrapper{width:100%;height:100%;overflow:hidden;position:relative}.graph-view .graph .sigma-container{position:absolute;width:100vw;height:100%;left:50%;margin-left:-50vw;background:#fcfcfc}.graph-view .graph .sigma-container .sigma-mouse{z-index:101}.graph-view .graph>*{z-index:1}.graph-view .graph .sigma{z-index:0}.graph-view .context-panel{background:#fff}.graph-view .toggle-button{position:absolute;top:1rem;left:1rem;z-index:103}.search-node.active-node,.search-node:hover{background:#eee;cursor:pointer}.terms-filter .term{margin-bottom:.1em;border-radius:5px;padding:2px}.terms-filter .term.editable .value{cursor:pointer}.terms-filter .term .value:hover{opacity:.7}.terms-filter .term .value span{color:#999}.terms-filter .term.active .value span{color:#333}.terms-filter .bar{height:5px;position:relative}.terms-filter .bar .global,.terms-filter .bar .filtered{position:absolute;left:0;top:0;bottom:0;transition:width ease-in-out .2s}.terms-filter .bar .global{background:#dee2e6}.terms-filter .bar .filtered{background:#343a40}.range-filter{height:160px;display:flex;flex-direction:row;justify-content:space-between}.range-filter .bar{position:relative;height:100%;flex-grow:1}.range-filter .bar:not(:last-child){margin-right:1px}.range-filter .global,.range-filter .filtered{position:absolute;left:0;right:0;bottom:0;transition:height ease-in-out .2s}.range-filter .global{background:#dee2e6}.range-filter .filtered{background:#343a40}.range-filter .label{position:absolute;text-align:center;width:100%;font-size:.8em}.range-filter .label.inside{top:0}.range-filter .label.outside{bottom:100%;color:var(--bs-secondary-color)}.rc-slider-mark-text{color:#999!important}.rc-slider-mark-text-active{color:#333!important}.fade-enter{opacity:0;transform:scale(.9)}.fade-enter-active{opacity:1;transform:translate(0);transition:opacity .3s,transform .3s}.fade-exit{opacity:1}.fade-exit-active{opacity:0;transform:scale(.9);transition:opacity .3s,transform .3s} diff --git a/dist/assets/index-COjAeYQr.js b/dist/assets/index-COjAeYQr.js new file mode 100644 index 0000000..4aedf8e --- /dev/null +++ b/dist/assets/index-COjAeYQr.js @@ -0,0 +1,542 @@ +function m3(e,t){for(var n=0;nr[i]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const u of s.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&r(u)}).observe(document,{childList:!0,subtree:!0});function n(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function r(i){if(i.ep)return;i.ep=!0;const s=n(i);fetch(i.href,s)}})();var $s=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function Jn(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var sv={exports:{}},Nd={},lv={exports:{}},_t={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var j1;function g3(){if(j1)return _t;j1=1;var e=Symbol.for("react.element"),t=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),s=Symbol.for("react.provider"),u=Symbol.for("react.context"),f=Symbol.for("react.forward_ref"),h=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),g=Symbol.for("react.lazy"),b=Symbol.iterator;function y(q){return q===null||typeof q!="object"?null:(q=b&&q[b]||q["@@iterator"],typeof q=="function"?q:null)}var w={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},_=Object.assign,k={};function C(q,J,se){this.props=q,this.context=J,this.refs=k,this.updater=se||w}C.prototype.isReactComponent={},C.prototype.setState=function(q,J){if(typeof q!="object"&&typeof q!="function"&&q!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,q,J,"setState")},C.prototype.forceUpdate=function(q){this.updater.enqueueForceUpdate(this,q,"forceUpdate")};function L(){}L.prototype=C.prototype;function A(q,J,se){this.props=q,this.context=J,this.refs=k,this.updater=se||w}var N=A.prototype=new L;N.constructor=A,_(N,C.prototype),N.isPureReactComponent=!0;var O=Array.isArray,T=Object.prototype.hasOwnProperty,M={current:null},D={key:!0,ref:!0,__self:!0,__source:!0};function V(q,J,se){var be,he={},ve=null,Se=null;if(J!=null)for(be in J.ref!==void 0&&(Se=J.ref),J.key!==void 0&&(ve=""+J.key),J)T.call(J,be)&&!D.hasOwnProperty(be)&&(he[be]=J[be]);var ke=arguments.length-2;if(ke===1)he.children=se;else if(1>>1,J=B[q];if(0>>1;qi(he,ne))vei(Se,he)?(B[q]=Se,B[ve]=ne,q=ve):(B[q]=he,B[be]=ne,q=be);else if(vei(Se,ne))B[q]=Se,B[ve]=ne,q=ve;else break e}}return Q}function i(B,Q){var ne=B.sortIndex-Q.sortIndex;return ne!==0?ne:B.id-Q.id}if(typeof performance=="object"&&typeof performance.now=="function"){var s=performance;e.unstable_now=function(){return s.now()}}else{var u=Date,f=u.now();e.unstable_now=function(){return u.now()-f}}var h=[],p=[],g=1,b=null,y=3,w=!1,_=!1,k=!1,C=typeof setTimeout=="function"?setTimeout:null,L=typeof clearTimeout=="function"?clearTimeout:null,A=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function N(B){for(var Q=n(p);Q!==null;){if(Q.callback===null)r(p);else if(Q.startTime<=B)r(p),Q.sortIndex=Q.expirationTime,t(h,Q);else break;Q=n(p)}}function O(B){if(k=!1,N(B),!_)if(n(h)!==null)_=!0,W(T);else{var Q=n(p);Q!==null&&j(O,Q.startTime-B)}}function T(B,Q){_=!1,k&&(k=!1,L(V),V=-1),w=!0;var ne=y;try{for(N(Q),b=n(h);b!==null&&(!(b.expirationTime>Q)||B&&!Y());){var q=b.callback;if(typeof q=="function"){b.callback=null,y=b.priorityLevel;var J=q(b.expirationTime<=Q);Q=e.unstable_now(),typeof J=="function"?b.callback=J:b===n(h)&&r(h),N(Q)}else r(h);b=n(h)}if(b!==null)var se=!0;else{var be=n(p);be!==null&&j(O,be.startTime-Q),se=!1}return se}finally{b=null,y=ne,w=!1}}var M=!1,D=null,V=-1,K=5,H=-1;function Y(){return!(e.unstable_now()-HB||125q?(B.sortIndex=ne,t(p,B),n(h)===null&&B===n(p)&&(k?(L(V),V=-1):k=!0,j(O,ne-q))):(B.sortIndex=J,t(h,B),_||w||(_=!0,W(T))),B},e.unstable_shouldYield=Y,e.unstable_wrapCallback=function(B){var Q=y;return function(){var ne=y;y=Q;try{return B.apply(this,arguments)}finally{y=ne}}}}(fv)),fv}var V1;function x3(){return V1||(V1=1,cv.exports=b3()),cv.exports}/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var W1;function w3(){if(W1)return ti;W1=1;var e=uh(),t=x3();function n(a){for(var o="https://reactjs.org/docs/error-decoder.html?invariant="+a,d=1;d"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),h=Object.prototype.hasOwnProperty,p=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,g={},b={};function y(a){return h.call(b,a)?!0:h.call(g,a)?!1:p.test(a)?b[a]=!0:(g[a]=!0,!1)}function w(a,o,d,m){if(d!==null&&d.type===0)return!1;switch(typeof o){case"function":case"symbol":return!0;case"boolean":return m?!1:d!==null?!d.acceptsBooleans:(a=a.toLowerCase().slice(0,5),a!=="data-"&&a!=="aria-");default:return!1}}function _(a,o,d,m){if(o===null||typeof o>"u"||w(a,o,d,m))return!0;if(m)return!1;if(d!==null)switch(d.type){case 3:return!o;case 4:return o===!1;case 5:return isNaN(o);case 6:return isNaN(o)||1>o}return!1}function k(a,o,d,m,x,E,z){this.acceptsBooleans=o===2||o===3||o===4,this.attributeName=m,this.attributeNamespace=x,this.mustUseProperty=d,this.propertyName=a,this.type=o,this.sanitizeURL=E,this.removeEmptyString=z}var C={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(a){C[a]=new k(a,0,!1,a,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(a){var o=a[0];C[o]=new k(o,1,!1,a[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(a){C[a]=new k(a,2,!1,a.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(a){C[a]=new k(a,2,!1,a,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(a){C[a]=new k(a,3,!1,a.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(a){C[a]=new k(a,3,!0,a,null,!1,!1)}),["capture","download"].forEach(function(a){C[a]=new k(a,4,!1,a,null,!1,!1)}),["cols","rows","size","span"].forEach(function(a){C[a]=new k(a,6,!1,a,null,!1,!1)}),["rowSpan","start"].forEach(function(a){C[a]=new k(a,5,!1,a.toLowerCase(),null,!1,!1)});var L=/[\-:]([a-z])/g;function A(a){return a[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(a){var o=a.replace(L,A);C[o]=new k(o,1,!1,a,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(a){var o=a.replace(L,A);C[o]=new k(o,1,!1,a,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(a){var o=a.replace(L,A);C[o]=new k(o,1,!1,a,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(a){C[a]=new k(a,1,!1,a.toLowerCase(),null,!1,!1)}),C.xlinkHref=new k("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(a){C[a]=new k(a,1,!1,a.toLowerCase(),null,!0,!0)});function N(a,o,d,m){var x=C.hasOwnProperty(o)?C[o]:null;(x!==null?x.type!==0:m||!(2Z||x[z]!==E[Z]){var ee=` +`+x[z].replace(" at new "," at ");return a.displayName&&ee.includes("")&&(ee=ee.replace("",a.displayName)),ee}while(1<=z&&0<=Z);break}}}finally{se=!1,Error.prepareStackTrace=d}return(a=a?a.displayName||a.name:"")?J(a):""}function he(a){switch(a.tag){case 5:return J(a.type);case 16:return J("Lazy");case 13:return J("Suspense");case 19:return J("SuspenseList");case 0:case 2:case 15:return a=be(a.type,!1),a;case 11:return a=be(a.type.render,!1),a;case 1:return a=be(a.type,!0),a;default:return""}}function ve(a){if(a==null)return null;if(typeof a=="function")return a.displayName||a.name||null;if(typeof a=="string")return a;switch(a){case D:return"Fragment";case M:return"Portal";case K:return"Profiler";case V:return"StrictMode";case I:return"Suspense";case ae:return"SuspenseList"}if(typeof a=="object")switch(a.$$typeof){case Y:return(a.displayName||"Context")+".Consumer";case H:return(a._context.displayName||"Context")+".Provider";case $:var o=a.render;return a=a.displayName,a||(a=o.displayName||o.name||"",a=a!==""?"ForwardRef("+a+")":"ForwardRef"),a;case ue:return o=a.displayName||null,o!==null?o:ve(a.type)||"Memo";case W:o=a._payload,a=a._init;try{return ve(a(o))}catch{}}return null}function Se(a){var o=a.type;switch(a.tag){case 24:return"Cache";case 9:return(o.displayName||"Context")+".Consumer";case 10:return(o._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return a=o.render,a=a.displayName||a.name||"",o.displayName||(a!==""?"ForwardRef("+a+")":"ForwardRef");case 7:return"Fragment";case 5:return o;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return ve(o);case 8:return o===V?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof o=="function")return o.displayName||o.name||null;if(typeof o=="string")return o}return null}function ke(a){switch(typeof a){case"boolean":case"number":case"string":case"undefined":return a;case"object":return a;default:return""}}function je(a){var o=a.type;return(a=a.nodeName)&&a.toLowerCase()==="input"&&(o==="checkbox"||o==="radio")}function De(a){var o=je(a)?"checked":"value",d=Object.getOwnPropertyDescriptor(a.constructor.prototype,o),m=""+a[o];if(!a.hasOwnProperty(o)&&typeof d<"u"&&typeof d.get=="function"&&typeof d.set=="function"){var x=d.get,E=d.set;return Object.defineProperty(a,o,{configurable:!0,get:function(){return x.call(this)},set:function(z){m=""+z,E.call(this,z)}}),Object.defineProperty(a,o,{enumerable:d.enumerable}),{getValue:function(){return m},setValue:function(z){m=""+z},stopTracking:function(){a._valueTracker=null,delete a[o]}}}}function Me(a){a._valueTracker||(a._valueTracker=De(a))}function ze(a){if(!a)return!1;var o=a._valueTracker;if(!o)return!0;var d=o.getValue(),m="";return a&&(m=je(a)?a.checked?"true":"false":a.value),a=m,a!==d?(o.setValue(a),!0):!1}function ut(a){if(a=a||(typeof document<"u"?document:void 0),typeof a>"u")return null;try{return a.activeElement||a.body}catch{return a.body}}function Le(a,o){var d=o.checked;return ne({},o,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:d??a._wrapperState.initialChecked})}function Oe(a,o){var d=o.defaultValue==null?"":o.defaultValue,m=o.checked!=null?o.checked:o.defaultChecked;d=ke(o.value!=null?o.value:d),a._wrapperState={initialChecked:m,initialValue:d,controlled:o.type==="checkbox"||o.type==="radio"?o.checked!=null:o.value!=null}}function pe(a,o){o=o.checked,o!=null&&N(a,"checked",o,!1)}function pt(a,o){pe(a,o);var d=ke(o.value),m=o.type;if(d!=null)m==="number"?(d===0&&a.value===""||a.value!=d)&&(a.value=""+d):a.value!==""+d&&(a.value=""+d);else if(m==="submit"||m==="reset"){a.removeAttribute("value");return}o.hasOwnProperty("value")?gt(a,o.type,d):o.hasOwnProperty("defaultValue")&>(a,o.type,ke(o.defaultValue)),o.checked==null&&o.defaultChecked!=null&&(a.defaultChecked=!!o.defaultChecked)}function Ct(a,o,d){if(o.hasOwnProperty("value")||o.hasOwnProperty("defaultValue")){var m=o.type;if(!(m!=="submit"&&m!=="reset"||o.value!==void 0&&o.value!==null))return;o=""+a._wrapperState.initialValue,d||o===a.value||(a.value=o),a.defaultValue=o}d=a.name,d!==""&&(a.name=""),a.defaultChecked=!!a._wrapperState.initialChecked,d!==""&&(a.name=d)}function gt(a,o,d){(o!=="number"||ut(a.ownerDocument)!==a)&&(d==null?a.defaultValue=""+a._wrapperState.initialValue:a.defaultValue!==""+d&&(a.defaultValue=""+d))}var ot=Array.isArray;function st(a,o,d,m){if(a=a.options,o){o={};for(var x=0;x"+o.valueOf().toString()+"",o=wt.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;o.firstChild;)a.appendChild(o.firstChild)}});function Yt(a,o){if(o){var d=a.firstChild;if(d&&d===a.lastChild&&d.nodeType===3){d.nodeValue=o;return}}a.textContent=o}var rn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Vi=["Webkit","ms","Moz","O"];Object.keys(rn).forEach(function(a){Vi.forEach(function(o){o=o+a.charAt(0).toUpperCase()+a.substring(1),rn[o]=rn[a]})});function kr(a,o,d){return o==null||typeof o=="boolean"||o===""?"":d||typeof o!="number"||o===0||rn.hasOwnProperty(a)&&rn[a]?(""+o).trim():o+"px"}function ri(a,o){a=a.style;for(var d in o)if(o.hasOwnProperty(d)){var m=d.indexOf("--")===0,x=kr(d,o[d],m);d==="float"&&(d="cssFloat"),m?a.setProperty(d,x):a[d]=x}}var vi=ne({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function fr(a,o){if(o){if(vi[a]&&(o.children!=null||o.dangerouslySetInnerHTML!=null))throw Error(n(137,a));if(o.dangerouslySetInnerHTML!=null){if(o.children!=null)throw Error(n(60));if(typeof o.dangerouslySetInnerHTML!="object"||!("__html"in o.dangerouslySetInnerHTML))throw Error(n(61))}if(o.style!=null&&typeof o.style!="object")throw Error(n(62))}}function Mr(a,o){if(a.indexOf("-")===-1)return typeof o.is=="string";switch(a){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Wi=null;function wa(a){return a=a.target||a.srcElement||window,a.correspondingUseElement&&(a=a.correspondingUseElement),a.nodeType===3?a.parentNode:a}var zr=null,qi=null,ln=null;function Ki(a){if(a=Vn(a)){if(typeof zr!="function")throw Error(n(280));var o=a.stateNode;o&&(o=Mu(o),zr(a.stateNode,a.type,o))}}function io(a){qi?ln?ln.push(a):ln=[a]:qi=a}function Ws(){if(qi){var a=qi,o=ln;if(ln=qi=null,Ki(a),o)for(a=0;a>>=0,a===0?32:31-(lo(a)/el|0)|0}var ns=64,fu=4194304;function rs(a){switch(a&-a){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return a&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return a&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return a}}function uo(a,o){var d=a.pendingLanes;if(d===0)return 0;var m=0,x=a.suspendedLanes,E=a.pingedLanes,z=d&268435455;if(z!==0){var Z=z&~x;Z!==0?m=rs(Z):(E&=z,E!==0&&(m=rs(E)))}else z=d&~x,z!==0?m=rs(z):E!==0&&(m=rs(E));if(m===0)return 0;if(o!==0&&o!==m&&!(o&x)&&(x=m&-m,E=o&-o,x>=E||x===16&&(E&4194240)!==0))return o;if(m&4&&(m|=d&16),o=a.entangledLanes,o!==0)for(a=a.entanglements,o&=m;0d;d++)o.push(a);return o}function nl(a,o,d){a.pendingLanes|=o,o!==536870912&&(a.suspendedLanes=0,a.pingedLanes=0),a=a.eventTimes,o=31-bn(o),a[o]=d}function xh(a,o){var d=a.pendingLanes&~o;a.pendingLanes=o,a.suspendedLanes=0,a.pingedLanes=0,a.expiredLanes&=o,a.mutableReadLanes&=o,a.entangledLanes&=o,o=a.entanglements;var m=a.eventTimes;for(a=a.expirationTimes;0=Qi),Ph=" ",Fh=!1;function Ih(a,o){switch(a){case"keyup":return Tr.indexOf(o.keyCode)!==-1;case"keydown":return o.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Mh(a){return a=a.detail,typeof a=="object"&&"data"in a?a.data:null}var us=!1;function cs(a,o){switch(a){case"compositionend":return Mh(o);case"keypress":return o.which!==32?null:(Fh=!0,Ph);case"textInput":return a=o.data,a===Ph&&Fh?null:a;default:return null}}function wg(a,o){if(us)return a==="compositionend"||!sl&&Ih(a,o)?(a=bf(),yi=ol=an=null,us=!1,a):null;switch(a){case"paste":return null;case"keypress":if(!(o.ctrlKey||o.altKey||o.metaKey)||o.ctrlKey&&o.altKey){if(o.char&&1=o)return{node:d,offset:o-a};a=m}e:{for(;d;){if(d.nextSibling){d=d.nextSibling;break e}d=d.parentNode}d=void 0}d=lt(d)}}function xn(a,o){return a&&o?a===o?!0:a&&a.nodeType===3?!1:o&&o.nodeType===3?xn(a,o.parentNode):"contains"in a?a.contains(o):a.compareDocumentPosition?!!(a.compareDocumentPosition(o)&16):!1:!1}function Vt(){for(var a=window,o=ut();o instanceof a.HTMLIFrameElement;){try{var d=typeof o.contentWindow.location.href=="string"}catch{d=!1}if(d)a=o.contentWindow;else break;o=ut(a.document)}return o}function ll(a){var o=a&&a.nodeName&&a.nodeName.toLowerCase();return o&&(o==="input"&&(a.type==="text"||a.type==="search"||a.type==="tel"||a.type==="url"||a.type==="password")||o==="textarea"||a.contentEditable==="true")}function Rg(a){var o=Vt(),d=a.focusedElem,m=a.selectionRange;if(o!==d&&d&&d.ownerDocument&&xn(d.ownerDocument.documentElement,d)){if(m!==null&&ll(d)){if(o=m.start,a=m.end,a===void 0&&(a=o),"selectionStart"in d)d.selectionStart=o,d.selectionEnd=Math.min(a,d.value.length);else if(a=(o=d.ownerDocument||document)&&o.defaultView||window,a.getSelection){a=a.getSelection();var x=d.textContent.length,E=Math.min(m.start,x);m=m.end===void 0?E:Math.min(m.end,x),!a.extend&&E>m&&(x=m,m=E,E=x),x=Nt(d,E);var z=Nt(d,m);x&&z&&(a.rangeCount!==1||a.anchorNode!==x.node||a.anchorOffset!==x.offset||a.focusNode!==z.node||a.focusOffset!==z.offset)&&(o=o.createRange(),o.setStart(x.node,x.offset),a.removeAllRanges(),E>m?(a.addRange(o),a.extend(z.node,z.offset)):(o.setEnd(z.node,z.offset),a.addRange(o)))}}for(o=[],a=d;a=a.parentNode;)a.nodeType===1&&o.push({element:a,left:a.scrollLeft,top:a.scrollTop});for(typeof d.focus=="function"&&d.focus(),d=0;d=document.documentMode,Ji=null,Nf=null,bi=null,ds=!1;function ul(a,o,d){var m=d.window===d?d.document:d.nodeType===9?d:d.ownerDocument;ds||Ji==null||Ji!==ut(m)||(m=Ji,"selectionStart"in m&&ll(m)?m={start:m.selectionStart,end:m.selectionEnd}:(m=(m.ownerDocument&&m.ownerDocument.defaultView||window).getSelection(),m={anchorNode:m.anchorNode,anchorOffset:m.anchorOffset,focusNode:m.focusNode,focusOffset:m.focusOffset}),bi&&$e(bi,m)||(bi=m,m=Ou(Nf,"onSelect"),0xs||(a.current=Mf[xs],Mf[xs]=null,xs--)}function Xt(a,o){xs++,Mf[xs]=a.current,a.current=o}var Fa={},Wn=mr(Fa),gr=mr(!1),nr=Fa;function ws(a,o){var d=a.type.contextTypes;if(!d)return Fa;var m=a.stateNode;if(m&&m.__reactInternalMemoizedUnmaskedChildContext===o)return m.__reactInternalMemoizedMaskedChildContext;var x={},E;for(E in d)x[E]=o[E];return m&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=o,a.__reactInternalMemoizedMaskedChildContext=x),x}function vr(a){return a=a.childContextTypes,a!=null}function zu(){en(gr),en(Wn)}function qh(a,o,d){if(Wn.current!==Fa)throw Error(n(168));Xt(Wn,o),Xt(gr,d)}function Kh(a,o,d){var m=a.stateNode;if(o=o.childContextTypes,typeof m.getChildContext!="function")return d;m=m.getChildContext();for(var x in m)if(!(x in o))throw Error(n(108,Se(a)||"Unknown",x));return ne({},d,m)}function Ur(a){return a=(a=a.stateNode)&&a.__reactInternalMemoizedMergedChildContext||Fa,nr=Wn.current,Xt(Wn,a),Xt(gr,gr.current),!0}function Yh(a,o,d){var m=a.stateNode;if(!m)throw Error(n(169));d?(a=Kh(a,o,nr),m.__reactInternalMemoizedMergedChildContext=a,en(gr),en(Wn),Xt(Wn,a)):en(gr),Xt(gr,d)}var na=null,ju=!1,zf=!1;function Zh(a){na===null?na=[a]:na.push(a)}function yo(a){ju=!0,Zh(a)}function Ia(){if(!zf&&na!==null){zf=!0;var a=0,o=zt;try{var d=na;for(zt=1;a>=z,x-=z,_i=1<<32-bn(o)+x|d<rt?(Nn=Xe,Xe=null):Nn=Xe.sibling;var Pt=xe(le,Xe,ce[rt],Te);if(Pt===null){Xe===null&&(Xe=Nn);break}a&&Xe&&Pt.alternate===null&&o(le,Xe),re=E(Pt,re,rt),et===null?Ke=Pt:et.sibling=Pt,et=Pt,Xe=Nn}if(rt===ce.length)return d(le,Xe),tn&&xo(le,rt),Ke;if(Xe===null){for(;rtrt?(Nn=Xe,Xe=null):Nn=Xe.sibling;var Ka=xe(le,Xe,Pt.value,Te);if(Ka===null){Xe===null&&(Xe=Nn);break}a&&Xe&&Ka.alternate===null&&o(le,Xe),re=E(Ka,re,rt),et===null?Ke=Ka:et.sibling=Ka,et=Ka,Xe=Nn}if(Pt.done)return d(le,Xe),tn&&xo(le,rt),Ke;if(Xe===null){for(;!Pt.done;rt++,Pt=ce.next())Pt=Ce(le,Pt.value,Te),Pt!==null&&(re=E(Pt,re,rt),et===null?Ke=Pt:et.sibling=Pt,et=Pt);return tn&&xo(le,rt),Ke}for(Xe=m(le,Xe);!Pt.done;rt++,Pt=ce.next())Pt=Ge(Xe,le,rt,Pt.value,Te),Pt!==null&&(a&&Pt.alternate!==null&&Xe.delete(Pt.key===null?rt:Pt.key),re=E(Pt,re,rt),et===null?Ke=Pt:et.sibling=Pt,et=Pt);return a&&Xe.forEach(function(Yg){return o(le,Yg)}),tn&&xo(le,rt),Ke}function gn(le,re,ce,Te){if(typeof ce=="object"&&ce!==null&&ce.type===D&&ce.key===null&&(ce=ce.props.children),typeof ce=="object"&&ce!==null){switch(ce.$$typeof){case T:e:{for(var Ke=ce.key,et=re;et!==null;){if(et.key===Ke){if(Ke=ce.type,Ke===D){if(et.tag===7){d(le,et.sibling),re=x(et,ce.props.children),re.return=le,le=re;break e}}else if(et.elementType===Ke||typeof Ke=="object"&&Ke!==null&&Ke.$$typeof===W&&ep(Ke)===et.type){d(le,et.sibling),re=x(et,ce.props),re.ref=xl(le,et,ce),re.return=le,le=re;break e}d(le,et);break}else o(le,et);et=et.sibling}ce.type===D?(re=Oo(ce.props.children,le.mode,Te,ce.key),re.return=le,le=re):(Te=Ec(ce.type,ce.key,ce.props,null,le.mode,Te),Te.ref=xl(le,re,ce),Te.return=le,le=Te)}return z(le);case M:e:{for(et=ce.key;re!==null;){if(re.key===et)if(re.tag===4&&re.stateNode.containerInfo===ce.containerInfo&&re.stateNode.implementation===ce.implementation){d(le,re.sibling),re=x(re,ce.children||[]),re.return=le,le=re;break e}else{d(le,re);break}else o(le,re);re=re.sibling}re=Rd(ce,le.mode,Te),re.return=le,le=re}return z(le);case W:return et=ce._init,gn(le,re,et(ce._payload),Te)}if(ot(ce))return We(le,re,ce,Te);if(Q(ce))return qe(le,re,ce,Te);_o(le,ce)}return typeof ce=="string"&&ce!==""||typeof ce=="number"?(ce=""+ce,re!==null&&re.tag===6?(d(le,re.sibling),re=x(re,ce),re.return=le,le=re):(d(le,re),re=kd(ce,le.mode,Te),re.return=le,le=re),z(le)):d(le,re)}return gn}var fn=$f(!0),Bu=$f(!1),wl=mr(null),Nr=null,Ma=null,Es=null;function ia(){Es=Ma=Nr=null}function Hu(a){var o=wl.current;en(wl),a._currentValue=o}function Mn(a,o,d){for(;a!==null;){var m=a.alternate;if((a.childLanes&o)!==o?(a.childLanes|=o,m!==null&&(m.childLanes|=o)):m!==null&&(m.childLanes&o)!==o&&(m.childLanes|=o),a===d)break;a=a.return}}function za(a,o){Nr=a,Es=Ma=null,a=a.dependencies,a!==null&&a.firstContext!==null&&(a.lanes&o&&(ir=!0),a.firstContext=null)}function Vr(a){var o=a._currentValue;if(Es!==a)if(a={context:a,memoizedValue:o,next:null},Ma===null){if(Nr===null)throw Error(n(308));Ma=a,Nr.dependencies={lanes:0,firstContext:a}}else Ma=Ma.next=a;return o}var Eo=null;function Gf(a){Eo===null?Eo=[a]:Eo.push(a)}function Vu(a,o,d,m){var x=o.interleaved;return x===null?(d.next=d,Gf(o)):(d.next=x.next,x.next=d),o.interleaved=d,aa(a,m)}function aa(a,o){a.lanes|=o;var d=a.alternate;for(d!==null&&(d.lanes|=o),d=a,a=a.return;a!==null;)a.childLanes|=o,d=a.alternate,d!==null&&(d.childLanes|=o),d=a,a=a.return;return d.tag===3?d.stateNode:null}var Wr=!1;function Wu(a){a.updateQueue={baseState:a.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function tp(a,o){a=a.updateQueue,o.updateQueue===a&&(o.updateQueue={baseState:a.baseState,firstBaseUpdate:a.firstBaseUpdate,lastBaseUpdate:a.lastBaseUpdate,shared:a.shared,effects:a.effects})}function oa(a,o){return{eventTime:a,lane:o,tag:0,payload:null,callback:null,next:null}}function qr(a,o,d){var m=a.updateQueue;if(m===null)return null;if(m=m.shared,Tt&2){var x=m.pending;return x===null?o.next=o:(o.next=x.next,x.next=o),m.pending=o,aa(a,d)}return x=m.interleaved,x===null?(o.next=o,Gf(m)):(o.next=x.next,x.next=o),m.interleaved=o,aa(a,d)}function qu(a,o,d){if(o=o.updateQueue,o!==null&&(o=o.shared,(d&4194240)!==0)){var m=o.lanes;m&=a.pendingLanes,d|=m,o.lanes=d,rl(a,d)}}function np(a,o){var d=a.updateQueue,m=a.alternate;if(m!==null&&(m=m.updateQueue,d===m)){var x=null,E=null;if(d=d.firstBaseUpdate,d!==null){do{var z={eventTime:d.eventTime,lane:d.lane,tag:d.tag,payload:d.payload,callback:d.callback,next:null};E===null?x=E=z:E=E.next=z,d=d.next}while(d!==null);E===null?x=E=o:E=E.next=o}else x=E=o;d={baseState:m.baseState,firstBaseUpdate:x,lastBaseUpdate:E,shared:m.shared,effects:m.effects},a.updateQueue=d;return}a=d.lastBaseUpdate,a===null?d.firstBaseUpdate=o:a.next=o,d.lastBaseUpdate=o}function Ss(a,o,d,m){var x=a.updateQueue;Wr=!1;var E=x.firstBaseUpdate,z=x.lastBaseUpdate,Z=x.shared.pending;if(Z!==null){x.shared.pending=null;var ee=Z,de=ee.next;ee.next=null,z===null?E=de:z.next=de,z=ee;var we=a.alternate;we!==null&&(we=we.updateQueue,Z=we.lastBaseUpdate,Z!==z&&(Z===null?we.firstBaseUpdate=de:Z.next=de,we.lastBaseUpdate=ee))}if(E!==null){var Ce=x.baseState;z=0,we=de=ee=null,Z=E;do{var xe=Z.lane,Ge=Z.eventTime;if((m&xe)===xe){we!==null&&(we=we.next={eventTime:Ge,lane:0,tag:Z.tag,payload:Z.payload,callback:Z.callback,next:null});e:{var We=a,qe=Z;switch(xe=o,Ge=d,qe.tag){case 1:if(We=qe.payload,typeof We=="function"){Ce=We.call(Ge,Ce,xe);break e}Ce=We;break e;case 3:We.flags=We.flags&-65537|128;case 0:if(We=qe.payload,xe=typeof We=="function"?We.call(Ge,Ce,xe):We,xe==null)break e;Ce=ne({},Ce,xe);break e;case 2:Wr=!0}}Z.callback!==null&&Z.lane!==0&&(a.flags|=64,xe=x.effects,xe===null?x.effects=[Z]:xe.push(Z))}else Ge={eventTime:Ge,lane:xe,tag:Z.tag,payload:Z.payload,callback:Z.callback,next:null},we===null?(de=we=Ge,ee=Ce):we=we.next=Ge,z|=xe;if(Z=Z.next,Z===null){if(Z=x.shared.pending,Z===null)break;xe=Z,Z=xe.next,xe.next=null,x.lastBaseUpdate=xe,x.shared.pending=null}}while(!0);if(we===null&&(ee=Ce),x.baseState=ee,x.firstBaseUpdate=de,x.lastBaseUpdate=we,o=x.shared.interleaved,o!==null){x=o;do z|=x.lane,x=x.next;while(x!==o)}else E===null&&(x.shared.lanes=0);Ba|=z,a.lanes=z,a.memoizedState=Ce}}function Uf(a,o,d){if(a=o.effects,o.effects=null,a!==null)for(o=0;od?d:4,a(!0);var m=Wf.transition;Wf.transition={};try{a(!1),o()}finally{zt=d,Wf.transition=m}}function ed(){return Kr().memoizedState}function Ag(a,o,d){var m=Wa(a);if(d={lane:m,action:d,hasEagerState:!1,eagerState:null,next:null},td(a))rr(o,d);else if(d=Vu(a,o,d,m),d!==null){var x=sr();fi(d,a,m,x),oi(d,o,m)}}function lp(a,o,d){var m=Wa(a),x={lane:m,action:d,hasEagerState:!1,eagerState:null,next:null};if(td(a))rr(o,x);else{var E=a.alternate;if(a.lanes===0&&(E===null||E.lanes===0)&&(E=o.lastRenderedReducer,E!==null))try{var z=o.lastRenderedState,Z=E(z,d);if(x.hasEagerState=!0,x.eagerState=Z,fe(Z,z)){var ee=o.interleaved;ee===null?(x.next=x,Gf(o)):(x.next=ee.next,ee.next=x),o.interleaved=x;return}}catch{}finally{}d=Vu(a,o,x,m),d!==null&&(x=sr(),fi(d,a,m,x),oi(d,o,m))}}function td(a){var o=a.alternate;return a===un||o!==null&&o===un}function rr(a,o){Cl=ks=!0;var d=a.pending;d===null?o.next=o:(o.next=d.next,d.next=o),a.pending=o}function oi(a,o,d){if(d&4194240){var m=o.lanes;m&=a.pendingLanes,d|=m,o.lanes=d,rl(a,d)}}var tc={readContext:Vr,useCallback:Yn,useContext:Yn,useEffect:Yn,useImperativeHandle:Yn,useInsertionEffect:Yn,useLayoutEffect:Yn,useMemo:Yn,useReducer:Yn,useRef:Yn,useState:Yn,useDebugValue:Yn,useDeferredValue:Yn,useTransition:Yn,useMutableSource:Yn,useSyncExternalStore:Yn,useId:Yn,unstable_isNewReconciler:!1},Dg={readContext:Vr,useCallback:function(a,o){return Ti().memoizedState=[a,o===void 0?null:o],a},useContext:Vr,useEffect:ec,useImperativeHandle:function(a,o,d){return d=d!=null?d.concat([a]):null,Rl(4194308,4,Qf.bind(null,o,a),d)},useLayoutEffect:function(a,o){return Rl(4194308,4,a,o)},useInsertionEffect:function(a,o){return Rl(4,2,a,o)},useMemo:function(a,o){var d=Ti();return o=o===void 0?null:o,a=a(),d.memoizedState=[a,o],a},useReducer:function(a,o,d){var m=Ti();return o=d!==void 0?d(o):o,m.memoizedState=m.baseState=o,a={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:a,lastRenderedState:o},m.queue=a,a=a.dispatch=Ag.bind(null,un,a),[m.memoizedState,a]},useRef:function(a){var o=Ti();return a={current:a},o.memoizedState=a},useState:kl,useDebugValue:Tl,useDeferredValue:function(a){return Ti().memoizedState=a},useTransition:function(){var a=kl(!1),o=a[0];return a=sp.bind(null,a[1]),Ti().memoizedState=a,[o,a]},useMutableSource:function(){},useSyncExternalStore:function(a,o,d){var m=un,x=Ti();if(tn){if(d===void 0)throw Error(n(407));d=d()}else{if(d=o(),Ln===null)throw Error(n(349));$a&30||Zf(m,o,d)}x.memoizedState=d;var E={value:d,getSnapshot:o};return x.queue=E,ec(la.bind(null,m,E,a),[a]),m.flags|=2048,Ts(9,br.bind(null,m,E,d,o),void 0,null),d},useId:function(){var a=Ti(),o=Ln.identifierPrefix;if(tn){var d=Ei,m=_i;d=(m&~(1<<32-bn(m)-1)).toString(32)+d,o=":"+o+"R"+d,d=Co++,0<\/script>",a=a.removeChild(a.firstChild)):typeof m.is=="string"?a=z.createElement(d,{is:m.is}):(a=z.createElement(d),d==="select"&&(z=a,m.multiple?z.multiple=!0:m.size&&(z.size=m.size))):a=z.createElementNS(a,d),a[xi]=o,a[Pa]=m,jn(a,o,!1,!1),o.stateNode=a;e:{switch(z=Mr(d,m),d){case"dialog":Jt("cancel",a),Jt("close",a),x=m;break;case"iframe":case"object":case"embed":Jt("load",a),x=m;break;case"video":case"audio":for(x=0;xAo&&(o.flags|=128,m=!0,Fl(E,!1),o.lanes=4194304)}else{if(!m)if(a=So(z),a!==null){if(o.flags|=128,m=!0,d=a.updateQueue,d!==null&&(o.updateQueue=d,o.flags|=4),Fl(E,!0),E.tail===null&&E.tailMode==="hidden"&&!z.alternate&&!tn)return $n(o),null}else 2*Fe()-E.renderingStartTime>Ao&&d!==1073741824&&(o.flags|=128,m=!0,Fl(E,!1),o.lanes=4194304);E.isBackwards?(z.sibling=o.child,o.child=z):(d=E.last,d!==null?d.sibling=z:o.child=z,E.last=z)}return E.tail!==null?(o=E.tail,E.rendering=o,E.tail=o.sibling,E.renderingStartTime=Fe(),o.sibling=null,d=on.current,Xt(on,m?d&1|2:d&1),o):($n(o),null);case 22:case 23:return Sd(),m=o.memoizedState!==null,a!==null&&a.memoizedState!==null!==m&&(o.flags|=8192),m&&o.mode&1?Pr&1073741824&&($n(o),o.subtreeFlags&6&&(o.flags|=8192)):$n(o),null;case 24:return null;case 25:return null}throw Error(n(156,o.tag))}function Ng(a,o){switch(wo(o),o.tag){case 1:return vr(o.type)&&zu(),a=o.flags,a&65536?(o.flags=a&-65537|128,o):null;case 3:return ja(),en(gr),en(Wn),Yu(),a=o.flags,a&65536&&!(a&128)?(o.flags=a&-65537|128,o):null;case 5:return Ku(o),null;case 13:if(en(on),a=o.memoizedState,a!==null&&a.dehydrated!==null){if(o.alternate===null)throw Error(n(340));Ci()}return a=o.flags,a&65536?(o.flags=a&-65537|128,o):null;case 19:return en(on),null;case 4:return ja(),null;case 10:return Hu(o.type._context),null;case 22:case 23:return Sd(),null;case 24:return null;default:return null}}var cc=!1,sn=!1,ar=typeof WeakSet=="function"?WeakSet:Set,Ve=null;function Ps(a,o){var d=a.ref;if(d!==null)if(typeof d=="function")try{d(null)}catch(m){cn(a,o,m)}else d.current=null}function Il(a,o,d){try{d()}catch(m){cn(a,o,m)}}var mp=!1;function Og(a,o){if(ml=gu,a=Vt(),ll(a)){if("selectionStart"in a)var d={start:a.selectionStart,end:a.selectionEnd};else e:{d=(d=a.ownerDocument)&&d.defaultView||window;var m=d.getSelection&&d.getSelection();if(m&&m.rangeCount!==0){d=m.anchorNode;var x=m.anchorOffset,E=m.focusNode;m=m.focusOffset;try{d.nodeType,E.nodeType}catch{d=null;break e}var z=0,Z=-1,ee=-1,de=0,we=0,Ce=a,xe=null;t:for(;;){for(var Ge;Ce!==d||x!==0&&Ce.nodeType!==3||(Z=z+x),Ce!==E||m!==0&&Ce.nodeType!==3||(ee=z+m),Ce.nodeType===3&&(z+=Ce.nodeValue.length),(Ge=Ce.firstChild)!==null;)xe=Ce,Ce=Ge;for(;;){if(Ce===a)break t;if(xe===d&&++de===x&&(Z=z),xe===E&&++we===m&&(ee=z),(Ge=Ce.nextSibling)!==null)break;Ce=xe,xe=Ce.parentNode}Ce=Ge}d=Z===-1||ee===-1?null:{start:Z,end:ee}}else d=null}d=d||{start:0,end:0}}else d=null;for(vo={focusedElem:a,selectionRange:d},gu=!1,Ve=o;Ve!==null;)if(o=Ve,a=o.child,(o.subtreeFlags&1028)!==0&&a!==null)a.return=o,Ve=a;else for(;Ve!==null;){o=Ve;try{var We=o.alternate;if(o.flags&1024)switch(o.tag){case 0:case 11:case 15:break;case 1:if(We!==null){var qe=We.memoizedProps,gn=We.memoizedState,le=o.stateNode,re=le.getSnapshotBeforeUpdate(o.elementType===o.type?qe:Yr(o.type,qe),gn);le.__reactInternalSnapshotBeforeUpdate=re}break;case 3:var ce=o.stateNode.containerInfo;ce.nodeType===1?ce.textContent="":ce.nodeType===9&&ce.documentElement&&ce.removeChild(ce.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(n(163))}}catch(Te){cn(o,o.return,Te)}if(a=o.sibling,a!==null){a.return=o.return,Ve=a;break}Ve=o.return}return We=mp,mp=!1,We}function fa(a,o,d){var m=o.updateQueue;if(m=m!==null?m.lastEffect:null,m!==null){var x=m=m.next;do{if((x.tag&a)===a){var E=x.destroy;x.destroy=void 0,E!==void 0&&Il(o,d,E)}x=x.next}while(x!==m)}}function Ml(a,o){if(o=o.updateQueue,o=o!==null?o.lastEffect:null,o!==null){var d=o=o.next;do{if((d.tag&a)===a){var m=d.create;d.destroy=m()}d=d.next}while(d!==o)}}function fc(a){var o=a.ref;if(o!==null){var d=a.stateNode;switch(a.tag){case 5:a=d;break;default:a=d}typeof o=="function"?o(a):o.current=a}}function gp(a){var o=a.alternate;o!==null&&(a.alternate=null,gp(o)),a.child=null,a.deletions=null,a.sibling=null,a.tag===5&&(o=a.stateNode,o!==null&&(delete o[xi],delete o[Pa],delete o[Iu],delete o[G],delete o[bs])),a.stateNode=null,a.return=null,a.dependencies=null,a.memoizedProps=null,a.memoizedState=null,a.pendingProps=null,a.stateNode=null,a.updateQueue=null}function vp(a){return a.tag===5||a.tag===3||a.tag===4}function yp(a){e:for(;;){for(;a.sibling===null;){if(a.return===null||vp(a.return))return null;a=a.return}for(a.sibling.return=a.return,a=a.sibling;a.tag!==5&&a.tag!==6&&a.tag!==18;){if(a.flags&2||a.child===null||a.tag===4)continue e;a.child.return=a,a=a.child}if(!(a.flags&2))return a.stateNode}}function hd(a,o,d){var m=a.tag;if(m===5||m===6)a=a.stateNode,o?d.nodeType===8?d.parentNode.insertBefore(a,o):d.insertBefore(a,o):(d.nodeType===8?(o=d.parentNode,o.insertBefore(a,d)):(o=d,o.appendChild(a)),d=d._reactRootContainer,d!=null||o.onclick!==null||(o.onclick=Pu));else if(m!==4&&(a=a.child,a!==null))for(hd(a,o,d),a=a.sibling;a!==null;)hd(a,o,d),a=a.sibling}function dc(a,o,d){var m=a.tag;if(m===5||m===6)a=a.stateNode,o?d.insertBefore(a,o):d.appendChild(a);else if(m!==4&&(a=a.child,a!==null))for(dc(a,o,d),a=a.sibling;a!==null;)dc(a,o,d),a=a.sibling}var Dn=null,li=!1;function Ni(a,o,d){for(d=d.child;d!==null;)pd(a,o,d),d=d.sibling}function pd(a,o,d){if(An&&typeof An.onCommitFiberUnmount=="function")try{An.onCommitFiberUnmount(hr,d)}catch{}switch(d.tag){case 5:sn||Ps(d,o);case 6:var m=Dn,x=li;Dn=null,Ni(a,o,d),Dn=m,li=x,Dn!==null&&(li?(a=Dn,d=d.stateNode,a.nodeType===8?a.parentNode.removeChild(d):a.removeChild(d)):Dn.removeChild(d.stateNode));break;case 18:Dn!==null&&(li?(a=Dn,d=d.stateNode,a.nodeType===8?If(a.parentNode,d):a.nodeType===1&&If(a,d),jt(a)):If(Dn,d.stateNode));break;case 4:m=Dn,x=li,Dn=d.stateNode.containerInfo,li=!0,Ni(a,o,d),Dn=m,li=x;break;case 0:case 11:case 14:case 15:if(!sn&&(m=d.updateQueue,m!==null&&(m=m.lastEffect,m!==null))){x=m=m.next;do{var E=x,z=E.destroy;E=E.tag,z!==void 0&&(E&2||E&4)&&Il(d,o,z),x=x.next}while(x!==m)}Ni(a,o,d);break;case 1:if(!sn&&(Ps(d,o),m=d.stateNode,typeof m.componentWillUnmount=="function"))try{m.props=d.memoizedProps,m.state=d.memoizedState,m.componentWillUnmount()}catch(Z){cn(d,o,Z)}Ni(a,o,d);break;case 21:Ni(a,o,d);break;case 22:d.mode&1?(sn=(m=sn)||d.memoizedState!==null,Ni(a,o,d),sn=m):Ni(a,o,d);break;default:Ni(a,o,d)}}function Fs(a){var o=a.updateQueue;if(o!==null){a.updateQueue=null;var d=a.stateNode;d===null&&(d=a.stateNode=new ar),o.forEach(function(m){var x=$g.bind(null,a,m);d.has(m)||(d.add(m),m.then(x,x))})}}function Or(a,o){var d=o.deletions;if(d!==null)for(var m=0;mx&&(x=z),m&=~E}if(m=x,m=Fe()-m,m=(120>m?120:480>m?480:1080>m?1080:1920>m?1920:3e3>m?3e3:4320>m?4320:1960*xp(m/1960))-m,10a?16:a,Va===null)var m=!1;else{if(a=Va,Va=null,or=0,Tt&6)throw Error(n(331));var x=Tt;for(Tt|=4,Ve=a.current;Ve!==null;){var E=Ve,z=E.child;if(Ve.flags&16){var Z=E.deletions;if(Z!==null){for(var ee=0;eeFe()-yd?Lo(a,0):mc|=d),wr(a,o)}function kp(a,o){o===0&&(a.mode&1?(o=fu,fu<<=1,!(fu&130023424)&&(fu=4194304)):o=1);var d=sr();a=aa(a,o),a!==null&&(nl(a,o,d),wr(a,d))}function jg(a){var o=a.memoizedState,d=0;o!==null&&(d=o.retryLane),kp(a,d)}function $g(a,o){var d=0;switch(a.tag){case 13:var m=a.stateNode,x=a.memoizedState;x!==null&&(d=x.retryLane);break;case 19:m=a.stateNode;break;default:throw Error(n(314))}m!==null&&m.delete(o),kp(a,d)}var Rp;Rp=function(a,o,d){if(a!==null)if(a.memoizedProps!==o.pendingProps||gr.current)ir=!0;else{if(!(a.lanes&d)&&!(o.flags&128))return ir=!1,hp(a,o,d);ir=!!(a.flags&131072)}else ir=!1,tn&&o.flags&1048576&&Xh(o,Gu,o.index);switch(o.lanes=0,o.tag){case 2:var m=o.type;uc(a,o),a=o.pendingProps;var x=ws(o,Wn.current);za(o,d),x=ko(null,o,m,a,x,d);var E=Zu();return o.flags|=1,typeof x=="object"&&x!==null&&typeof x.render=="function"&&x.$$typeof===void 0?(o.tag=1,o.memoizedState=null,o.updateQueue=null,vr(m)?(E=!0,Ur(o)):E=!1,o.memoizedState=x.state!==null&&x.state!==void 0?x.state:null,Wu(o),x.updater=ic,o.stateNode=x,x._reactInternals=o,rd(o,m,a,d),o=cd(null,o,m,!0,E,d)):(o.tag=0,tn&&E&&yl(o),zn(null,o,x,d),o=o.child),o;case 16:m=o.elementType;e:{switch(uc(a,o),a=o.pendingProps,x=m._init,m=x(m._payload),o.type=m,x=o.tag=Ug(m),a=Yr(m,a),x){case 0:o=ld(null,o,m,a,d);break e;case 1:o=ud(null,o,m,a,d);break e;case 11:o=fp(null,o,m,a,d);break e;case 14:o=ad(null,o,m,Yr(m.type,a),d);break e}throw Error(n(306,m,""))}return o;case 0:return m=o.type,x=o.pendingProps,x=o.elementType===m?x:Yr(m,x),ld(a,o,m,x,d);case 1:return m=o.type,x=o.pendingProps,x=o.elementType===m?x:Yr(m,x),ud(a,o,m,x,d);case 3:e:{if(dp(o),a===null)throw Error(n(387));m=o.pendingProps,E=o.memoizedState,x=E.element,tp(a,o),Ss(o,m,null,d);var z=o.memoizedState;if(m=z.element,E.isDehydrated)if(E={element:m,isDehydrated:!1,cache:z.cache,pendingSuspenseBoundaries:z.pendingSuspenseBoundaries,transitions:z.transitions},o.updateQueue.baseState=E,o.memoizedState=E,o.flags&256){x=To(Error(n(423)),o),o=Li(a,o,m,d,x);break e}else if(m!==x){x=To(Error(n(424)),o),o=Li(a,o,m,d,x);break e}else for(Lr=Oa(o.stateNode.containerInfo.firstChild),Kn=o,tn=!0,ai=null,d=Bu(o,null,m,d),o.child=d;d;)d.flags=d.flags&-3|4096,d=d.sibling;else{if(Ci(),m===x){o=si(a,o,d);break e}zn(a,o,m,d)}o=o.child}return o;case 5:return Hf(o),a===null&&yr(o),m=o.type,x=o.pendingProps,E=a!==null?a.memoizedProps:null,z=x.children,gl(m,x)?z=null:E!==null&&gl(m,E)&&(o.flags|=32),sd(a,o),zn(a,o,z,d),o.child;case 6:return a===null&&yr(o),null;case 13:return lc(a,o,d);case 4:return Bf(o,o.stateNode.containerInfo),m=o.pendingProps,a===null?o.child=fn(o,null,m,d):zn(a,o,m,d),o.child;case 11:return m=o.type,x=o.pendingProps,x=o.elementType===m?x:Yr(m,x),fp(a,o,m,x,d);case 7:return zn(a,o,o.pendingProps,d),o.child;case 8:return zn(a,o,o.pendingProps.children,d),o.child;case 12:return zn(a,o,o.pendingProps.children,d),o.child;case 10:e:{if(m=o.type._context,x=o.pendingProps,E=o.memoizedProps,z=x.value,Xt(wl,m._currentValue),m._currentValue=z,E!==null)if(fe(E.value,z)){if(E.children===x.children&&!gr.current){o=si(a,o,d);break e}}else for(E=o.child,E!==null&&(E.return=o);E!==null;){var Z=E.dependencies;if(Z!==null){z=E.child;for(var ee=Z.firstContext;ee!==null;){if(ee.context===m){if(E.tag===1){ee=oa(-1,d&-d),ee.tag=2;var de=E.updateQueue;if(de!==null){de=de.shared;var we=de.pending;we===null?ee.next=ee:(ee.next=we.next,we.next=ee),de.pending=ee}}E.lanes|=d,ee=E.alternate,ee!==null&&(ee.lanes|=d),Mn(E.return,d,o),Z.lanes|=d;break}ee=ee.next}}else if(E.tag===10)z=E.type===o.type?null:E.child;else if(E.tag===18){if(z=E.return,z===null)throw Error(n(341));z.lanes|=d,Z=z.alternate,Z!==null&&(Z.lanes|=d),Mn(z,d,o),z=E.sibling}else z=E.child;if(z!==null)z.return=E;else for(z=E;z!==null;){if(z===o){z=null;break}if(E=z.sibling,E!==null){E.return=z.return,z=E;break}z=z.return}E=z}zn(a,o,x.children,d),o=o.child}return o;case 9:return x=o.type,m=o.pendingProps.children,za(o,d),x=Vr(x),m=m(x),o.flags|=1,zn(a,o,m,d),o.child;case 14:return m=o.type,x=Yr(m,o.pendingProps),x=Yr(m.type,x),ad(a,o,m,x,d);case 15:return Di(a,o,o.type,o.pendingProps,d);case 17:return m=o.type,x=o.pendingProps,x=o.elementType===m?x:Yr(m,x),uc(a,o),o.tag=1,vr(m)?(a=!0,Ur(o)):a=!1,za(o,d),Ro(o,m,x),rd(o,m,x,d),cd(null,o,m,!0,a,d);case 19:return Ga(a,o,d);case 22:return od(a,o,d)}throw Error(n(156,o.tag))};function Tp(a,o){return Qs(a,o)}function Gg(a,o,d,m){this.tag=a,this.key=d,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=o,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=m,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Xr(a,o,d,m){return new Gg(a,o,d,m)}function _c(a){return a=a.prototype,!(!a||!a.isReactComponent)}function Ug(a){if(typeof a=="function")return _c(a)?1:0;if(a!=null){if(a=a.$$typeof,a===$)return 11;if(a===ue)return 14}return 2}function di(a,o){var d=a.alternate;return d===null?(d=Xr(a.tag,o,a.key,a.mode),d.elementType=a.elementType,d.type=a.type,d.stateNode=a.stateNode,d.alternate=a,a.alternate=d):(d.pendingProps=o,d.type=a.type,d.flags=0,d.subtreeFlags=0,d.deletions=null),d.flags=a.flags&14680064,d.childLanes=a.childLanes,d.lanes=a.lanes,d.child=a.child,d.memoizedProps=a.memoizedProps,d.memoizedState=a.memoizedState,d.updateQueue=a.updateQueue,o=a.dependencies,d.dependencies=o===null?null:{lanes:o.lanes,firstContext:o.firstContext},d.sibling=a.sibling,d.index=a.index,d.ref=a.ref,d}function Ec(a,o,d,m,x,E){var z=2;if(m=a,typeof a=="function")_c(a)&&(z=1);else if(typeof a=="string")z=5;else e:switch(a){case D:return Oo(d.children,x,E,o);case V:z=8,x|=8;break;case K:return a=Xr(12,d,o,x|2),a.elementType=K,a.lanes=E,a;case I:return a=Xr(13,d,o,x),a.elementType=I,a.lanes=E,a;case ae:return a=Xr(19,d,o,x),a.elementType=ae,a.lanes=E,a;case j:return Sc(d,x,E,o);default:if(typeof a=="object"&&a!==null)switch(a.$$typeof){case H:z=10;break e;case Y:z=9;break e;case $:z=11;break e;case ue:z=14;break e;case W:z=16,m=null;break e}throw Error(n(130,a==null?a:typeof a,""))}return o=Xr(z,d,o,x),o.elementType=a,o.type=m,o.lanes=E,o}function Oo(a,o,d,m){return a=Xr(7,a,m,o),a.lanes=d,a}function Sc(a,o,d,m){return a=Xr(22,a,m,o),a.elementType=j,a.lanes=d,a.stateNode={isHidden:!1},a}function kd(a,o,d){return a=Xr(6,a,null,o),a.lanes=d,a}function Rd(a,o,d){return o=Xr(4,a.children!==null?a.children:[],a.key,o),o.lanes=d,o.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation},o}function Bg(a,o,d,m,x){this.tag=o,this.containerInfo=a,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=tl(0),this.expirationTimes=tl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=tl(0),this.identifierPrefix=m,this.onRecoverableError=x,this.mutableSourceEagerHydrationData=null}function Td(a,o,d,m,x,E,z,Z,ee){return a=new Bg(a,o,d,Z,ee),o===1?(o=1,E===!0&&(o|=8)):o=0,E=Xr(3,null,null,o),a.current=E,E.stateNode=a,E.memoizedState={element:m,isDehydrated:d,cache:null,transitions:null,pendingSuspenseBoundaries:null},Wu(E),a}function Hg(a,o,d){var m=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),uv.exports=w3(),uv.exports}var K1;function _3(){if(K1)return Mp;K1=1;var e=d2();return Mp.createRoot=e.createRoot,Mp.hydrateRoot=e.hydrateRoot,Mp}var E3=_3();const Km=P.createContext({portalTarget:document.createElement("div")}),In=P.createContext(null);var Od={},Y1;function S3(){if(Y1)return Od;Y1=1,Object.defineProperty(Od,"__esModule",{value:!0}),Od.parse=u,Od.serialize=p;const e=/^[\u0021-\u003A\u003C\u003E-\u007E]+$/,t=/^[\u0021-\u003A\u003C-\u007E]*$/,n=/^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i,r=/^[\u0020-\u003A\u003D-\u007E]*$/,i=Object.prototype.toString,s=(()=>{const y=function(){};return y.prototype=Object.create(null),y})();function u(y,w){const _=new s,k=y.length;if(k<2)return _;const C=(w==null?void 0:w.decode)||g;let L=0;do{const A=y.indexOf("=",L);if(A===-1)break;const N=y.indexOf(";",L),O=N===-1?k:N;if(A>O){L=y.lastIndexOf(";",A-1)+1;continue}const T=f(y,L,A),M=h(y,A,T),D=y.slice(T,M);if(_[D]===void 0){let V=f(y,A+1,O),K=h(y,O,V);const H=C(y.slice(V,K));_[D]=H}L=O+1}while(L_;){const k=y.charCodeAt(--w);if(k!==32&&k!==9)return w+1}return _}function p(y,w,_){const k=(_==null?void 0:_.encode)||encodeURIComponent;if(!e.test(y))throw new TypeError(`argument name is invalid: ${y}`);const C=k(w);if(!t.test(C))throw new TypeError(`argument val is invalid: ${w}`);let L=y+"="+C;if(!_)return L;if(_.maxAge!==void 0){if(!Number.isInteger(_.maxAge))throw new TypeError(`option maxAge is invalid: ${_.maxAge}`);L+="; Max-Age="+_.maxAge}if(_.domain){if(!n.test(_.domain))throw new TypeError(`option domain is invalid: ${_.domain}`);L+="; Domain="+_.domain}if(_.path){if(!r.test(_.path))throw new TypeError(`option path is invalid: ${_.path}`);L+="; Path="+_.path}if(_.expires){if(!b(_.expires)||!Number.isFinite(_.expires.valueOf()))throw new TypeError(`option expires is invalid: ${_.expires}`);L+="; Expires="+_.expires.toUTCString()}if(_.httpOnly&&(L+="; HttpOnly"),_.secure&&(L+="; Secure"),_.partitioned&&(L+="; Partitioned"),_.priority)switch(typeof _.priority=="string"?_.priority.toLowerCase():void 0){case"low":L+="; Priority=Low";break;case"medium":L+="; Priority=Medium";break;case"high":L+="; Priority=High";break;default:throw new TypeError(`option priority is invalid: ${_.priority}`)}if(_.sameSite)switch(typeof _.sameSite=="string"?_.sameSite.toLowerCase():_.sameSite){case!0:case"strict":L+="; SameSite=Strict";break;case"lax":L+="; SameSite=Lax";break;case"none":L+="; SameSite=None";break;default:throw new TypeError(`option sameSite is invalid: ${_.sameSite}`)}return L}function g(y){if(y.indexOf("%")===-1)return y;try{return decodeURIComponent(y)}catch{return y}}function b(y){return i.call(y)==="[object Date]"}return Od}S3();/** + * react-router v7.2.0 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */var Z1="popstate";function C3(e={}){function t(i,s){let{pathname:u="/",search:f="",hash:h=""}=iu(i.location.hash.substring(1));return!u.startsWith("/")&&!u.startsWith(".")&&(u="/"+u),$0("",{pathname:u,search:f,hash:h},s.state&&s.state.usr||null,s.state&&s.state.key||"default")}function n(i,s){let u=i.document.querySelector("base"),f="";if(u&&u.getAttribute("href")){let h=i.location.href,p=h.indexOf("#");f=p===-1?h:h.slice(0,p)}return f+"#"+(typeof s=="string"?s:th(s))}function r(i,s){zi(i.pathname.charAt(0)==="/",`relative pathnames are not supported in hash history.push(${JSON.stringify(s)})`)}return R3(t,n,r,e)}function hn(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function zi(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function k3(){return Math.random().toString(36).substring(2,10)}function X1(e,t){return{usr:e.state,key:e.key,idx:t}}function $0(e,t,n=null,r){return{pathname:typeof e=="string"?e:e.pathname,search:"",hash:"",...typeof t=="string"?iu(t):t,state:n,key:t&&t.key||r||k3()}}function th({pathname:e="/",search:t="",hash:n=""}){return t&&t!=="?"&&(e+=t.charAt(0)==="?"?t:"?"+t),n&&n!=="#"&&(e+=n.charAt(0)==="#"?n:"#"+n),e}function iu(e){let t={};if(e){let n=e.indexOf("#");n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf("?");r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function R3(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:s=!1}=r,u=i.history,f="POP",h=null,p=g();p==null&&(p=0,u.replaceState({...u.state,idx:p},""));function g(){return(u.state||{idx:null}).idx}function b(){f="POP";let C=g(),L=C==null?null:C-p;p=C,h&&h({action:f,location:k.location,delta:L})}function y(C,L){f="PUSH";let A=$0(k.location,C,L);n&&n(A,C),p=g()+1;let N=X1(A,p),O=k.createHref(A);try{u.pushState(N,"",O)}catch(T){if(T instanceof DOMException&&T.name==="DataCloneError")throw T;i.location.assign(O)}s&&h&&h({action:f,location:k.location,delta:1})}function w(C,L){f="REPLACE";let A=$0(k.location,C,L);n&&n(A,C),p=g();let N=X1(A,p),O=k.createHref(A);u.replaceState(N,"",O),s&&h&&h({action:f,location:k.location,delta:0})}function _(C){let L=i.location.origin!=="null"?i.location.origin:i.location.href,A=typeof C=="string"?C:th(C);return A=A.replace(/ $/,"%20"),hn(L,`No window.location.(origin|href) available to create URL for href: ${A}`),new URL(A,L)}let k={get action(){return f},get location(){return e(i,u)},listen(C){if(h)throw new Error("A history only accepts one active listener");return i.addEventListener(Z1,b),h=C,()=>{i.removeEventListener(Z1,b),h=null}},createHref(C){return t(i,C)},createURL:_,encodeLocation(C){let L=_(C);return{pathname:L.pathname,search:L.search,hash:L.hash}},push:y,replace:w,go(C){return u.go(C)}};return k}function h2(e,t,n="/"){return T3(e,t,n,!1)}function T3(e,t,n,r){let i=typeof t=="string"?iu(t):t,s=Us(i.pathname||"/",n);if(s==null)return null;let u=p2(e);A3(u);let f=null;for(let h=0;f==null&&h{let h={relativePath:f===void 0?s.path||"":f,caseSensitive:s.caseSensitive===!0,childrenIndex:u,route:s};h.relativePath.startsWith("/")&&(hn(h.relativePath.startsWith(r),`Absolute route path "${h.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),h.relativePath=h.relativePath.slice(r.length));let p=Ho([r,h.relativePath]),g=n.concat(h);s.children&&s.children.length>0&&(hn(s.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${p}".`),p2(s.children,t,g,p)),!(s.path==null&&!s.index)&&t.push({path:p,score:I3(p,s.index),routesMeta:g})};return e.forEach((s,u)=>{var f;if(s.path===""||!((f=s.path)!=null&&f.includes("?")))i(s,u);else for(let h of m2(s.path))i(s,u,h)}),t}function m2(e){let t=e.split("/");if(t.length===0)return[];let[n,...r]=t,i=n.endsWith("?"),s=n.replace(/\?$/,"");if(r.length===0)return i?[s,""]:[s];let u=m2(r.join("/")),f=[];return f.push(...u.map(h=>h===""?s:[s,h].join("/"))),i&&f.push(...u),f.map(h=>e.startsWith("/")&&h===""?"/":h)}function A3(e){e.sort((t,n)=>t.score!==n.score?n.score-t.score:M3(t.routesMeta.map(r=>r.childrenIndex),n.routesMeta.map(r=>r.childrenIndex)))}var D3=/^:[\w-]+$/,L3=3,N3=2,O3=1,P3=10,F3=-2,Q1=e=>e==="*";function I3(e,t){let n=e.split("/"),r=n.length;return n.some(Q1)&&(r+=F3),t&&(r+=N3),n.filter(i=>!Q1(i)).reduce((i,s)=>i+(D3.test(s)?L3:s===""?O3:P3),r)}function M3(e,t){return e.length===t.length&&e.slice(0,-1).every((r,i)=>r===t[i])?e[e.length-1]-t[t.length-1]:0}function z3(e,t,n=!1){let{routesMeta:r}=e,i={},s="/",u=[];for(let f=0;f{if(g==="*"){let _=f[y]||"";u=s.slice(0,s.length-_.length).replace(/(.)\/+$/,"$1")}const w=f[y];return b&&!w?p[g]=void 0:p[g]=(w||"").replace(/%2F/g,"/"),p},{}),pathname:s,pathnameBase:u,pattern:e}}function j3(e,t=!1,n=!0){zi(e==="*"||!e.endsWith("*")||e.endsWith("/*"),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,"/*")}".`);let r=[],i="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(u,f,h)=>(r.push({paramName:f,isOptional:h!=null}),h?"/?([^\\/]+)?":"/([^\\/]+)"));return e.endsWith("*")?(r.push({paramName:"*"}),i+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):n?i+="\\/*$":e!==""&&e!=="/"&&(i+="(?:(?=\\/|$))"),[new RegExp(i,t?void 0:"i"),r]}function $3(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return zi(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function Us(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith("/")?t.length-1:t.length,r=e.charAt(n);return r&&r!=="/"?null:e.slice(n)||"/"}function G3(e,t="/"){let{pathname:n,search:r="",hash:i=""}=typeof e=="string"?iu(e):e;return{pathname:n?n.startsWith("/")?n:U3(n,t):t,search:V3(r),hash:W3(i)}}function U3(e,t){let n=t.replace(/\/+$/,"").split("/");return e.split("/").forEach(i=>{i===".."?n.length>1&&n.pop():i!=="."&&n.push(i)}),n.length>1?n.join("/"):"/"}function dv(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function B3(e){return e.filter((t,n)=>n===0||t.route.path&&t.route.path.length>0)}function Cy(e){let t=B3(e);return t.map((n,r)=>r===t.length-1?n.pathname:n.pathnameBase)}function ky(e,t,n,r=!1){let i;typeof e=="string"?i=iu(e):(i={...e},hn(!i.pathname||!i.pathname.includes("?"),dv("?","pathname","search",i)),hn(!i.pathname||!i.pathname.includes("#"),dv("#","pathname","hash",i)),hn(!i.search||!i.search.includes("#"),dv("#","search","hash",i)));let s=e===""||i.pathname==="",u=s?"/":i.pathname,f;if(u==null)f=n;else{let b=t.length-1;if(!r&&u.startsWith("..")){let y=u.split("/");for(;y[0]==="..";)y.shift(),b-=1;i.pathname=y.join("/")}f=b>=0?t[b]:"/"}let h=G3(i,f),p=u&&u!=="/"&&u.endsWith("/"),g=(s||u===".")&&n.endsWith("/");return!h.pathname.endsWith("/")&&(p||g)&&(h.pathname+="/"),h}var Ho=e=>e.join("/").replace(/\/\/+/g,"/"),H3=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),V3=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,W3=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e;function q3(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}var g2=["POST","PUT","PATCH","DELETE"];new Set(g2);var K3=["GET",...g2];new Set(K3);var rf=P.createContext(null);rf.displayName="DataRouter";var Ym=P.createContext(null);Ym.displayName="DataRouterState";var v2=P.createContext({isTransitioning:!1});v2.displayName="ViewTransition";var Y3=P.createContext(new Map);Y3.displayName="Fetchers";var Z3=P.createContext(null);Z3.displayName="Await";var Gi=P.createContext(null);Gi.displayName="Navigation";var ch=P.createContext(null);ch.displayName="Location";var ro=P.createContext({outlet:null,matches:[],isDataRoute:!1});ro.displayName="Route";var Ry=P.createContext(null);Ry.displayName="RouteError";function X3(e,{relative:t}={}){hn(af(),"useHref() may be used only in the context of a component.");let{basename:n,navigator:r}=P.useContext(Gi),{hash:i,pathname:s,search:u}=fh(e,{relative:t}),f=s;return n!=="/"&&(f=s==="/"?n:Ho([n,s])),r.createHref({pathname:f,search:u,hash:i})}function af(){return P.useContext(ch)!=null}function xa(){return hn(af(),"useLocation() may be used only in the context of a component."),P.useContext(ch).location}var y2="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function b2(e){P.useContext(Gi).static||P.useLayoutEffect(e)}function Zm(){let{isDataRoute:e}=P.useContext(ro);return e?c5():Q3()}function Q3(){hn(af(),"useNavigate() may be used only in the context of a component.");let e=P.useContext(rf),{basename:t,navigator:n}=P.useContext(Gi),{matches:r}=P.useContext(ro),{pathname:i}=xa(),s=JSON.stringify(Cy(r)),u=P.useRef(!1);return b2(()=>{u.current=!0}),P.useCallback((h,p={})=>{if(zi(u.current,y2),!u.current)return;if(typeof h=="number"){n.go(h);return}let g=ky(h,JSON.parse(s),i,p.relative==="path");e==null&&t!=="/"&&(g.pathname=g.pathname==="/"?t:Ho([t,g.pathname])),(p.replace?n.replace:n.push)(g,p.state,p)},[t,n,s,i,e])}P.createContext(null);function fh(e,{relative:t}={}){let{matches:n}=P.useContext(ro),{pathname:r}=xa(),i=JSON.stringify(Cy(n));return P.useMemo(()=>ky(e,JSON.parse(i),r,t==="path"),[e,i,r,t])}function J3(e,t){return x2(e,t)}function x2(e,t,n,r){var A;hn(af(),"useRoutes() may be used only in the context of a component.");let{navigator:i,static:s}=P.useContext(Gi),{matches:u}=P.useContext(ro),f=u[u.length-1],h=f?f.params:{},p=f?f.pathname:"/",g=f?f.pathnameBase:"/",b=f&&f.route;{let N=b&&b.path||"";w2(p,!b||N.endsWith("*")||N.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${p}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let y=xa(),w;if(t){let N=typeof t=="string"?iu(t):t;hn(g==="/"||((A=N.pathname)==null?void 0:A.startsWith(g)),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${g}" but pathname "${N.pathname}" was given in the \`location\` prop.`),w=N}else w=y;let _=w.pathname||"/",k=_;if(g!=="/"){let N=g.replace(/^\//,"").split("/");k="/"+_.replace(/^\//,"").split("/").slice(N.length).join("/")}let C=!s&&n&&n.matches&&n.matches.length>0?n.matches:h2(e,{pathname:k});zi(b||C!=null,`No routes matched location "${w.pathname}${w.search}${w.hash}" `),zi(C==null||C[C.length-1].route.element!==void 0||C[C.length-1].route.Component!==void 0||C[C.length-1].route.lazy!==void 0,`Matched leaf route at location "${w.pathname}${w.search}${w.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let L=i5(C&&C.map(N=>Object.assign({},N,{params:Object.assign({},h,N.params),pathname:Ho([g,i.encodeLocation?i.encodeLocation(N.pathname).pathname:N.pathname]),pathnameBase:N.pathnameBase==="/"?g:Ho([g,i.encodeLocation?i.encodeLocation(N.pathnameBase).pathname:N.pathnameBase])})),u,n,r);return t&&L?P.createElement(ch.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",...w},navigationType:"POP"}},L):L}function e5(){let e=u5(),t=q3(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r="rgba(200,200,200, 0.5)",i={padding:"0.5rem",backgroundColor:r},s={padding:"2px 4px",backgroundColor:r},u=null;return console.error("Error handled by React Router default ErrorBoundary:",e),u=P.createElement(P.Fragment,null,P.createElement("p",null,"💿 Hey developer 👋"),P.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",P.createElement("code",{style:s},"ErrorBoundary")," or"," ",P.createElement("code",{style:s},"errorElement")," prop on your route.")),P.createElement(P.Fragment,null,P.createElement("h2",null,"Unexpected Application Error!"),P.createElement("h3",{style:{fontStyle:"italic"}},t),n?P.createElement("pre",{style:i},n):null,u)}var t5=P.createElement(e5,null),n5=class extends P.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!=="idle"&&e.revalidation==="idle"?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error!==void 0?e.error:t.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){console.error("React Router caught the following error during render",e,t)}render(){return this.state.error!==void 0?P.createElement(ro.Provider,{value:this.props.routeContext},P.createElement(Ry.Provider,{value:this.state.error,children:this.props.component})):this.props.children}};function r5({routeContext:e,match:t,children:n}){let r=P.useContext(rf);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),P.createElement(ro.Provider,{value:e},n)}function i5(e,t=[],n=null,r=null){if(e==null){if(!n)return null;if(n.errors)e=n.matches;else if(t.length===0&&!n.initialized&&n.matches.length>0)e=n.matches;else return null}let i=e,s=n==null?void 0:n.errors;if(s!=null){let h=i.findIndex(p=>p.route.id&&(s==null?void 0:s[p.route.id])!==void 0);hn(h>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(s).join(",")}`),i=i.slice(0,Math.min(i.length,h+1))}let u=!1,f=-1;if(n)for(let h=0;h=0?i=i.slice(0,f+1):i=[i[0]];break}}}return i.reduceRight((h,p,g)=>{let b,y=!1,w=null,_=null;n&&(b=s&&p.route.id?s[p.route.id]:void 0,w=p.route.errorElement||t5,u&&(f<0&&g===0?(w2("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),y=!0,_=null):f===g&&(y=!0,_=p.route.hydrateFallbackElement||null)));let k=t.concat(i.slice(0,g+1)),C=()=>{let L;return b?L=w:y?L=_:p.route.Component?L=P.createElement(p.route.Component,null):p.route.element?L=p.route.element:L=h,P.createElement(r5,{match:p,routeContext:{outlet:h,matches:k,isDataRoute:n!=null},children:L})};return n&&(p.route.ErrorBoundary||p.route.errorElement||g===0)?P.createElement(n5,{location:n.location,revalidation:n.revalidation,component:w,error:b,children:C(),routeContext:{outlet:null,matches:k,isDataRoute:!0}}):C()},null)}function Ty(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function a5(e){let t=P.useContext(rf);return hn(t,Ty(e)),t}function o5(e){let t=P.useContext(Ym);return hn(t,Ty(e)),t}function s5(e){let t=P.useContext(ro);return hn(t,Ty(e)),t}function Ay(e){let t=s5(e),n=t.matches[t.matches.length-1];return hn(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function l5(){return Ay("useRouteId")}function u5(){var r;let e=P.useContext(Ry),t=o5("useRouteError"),n=Ay("useRouteError");return e!==void 0?e:(r=t.errors)==null?void 0:r[n]}function c5(){let{router:e}=a5("useNavigate"),t=Ay("useNavigate"),n=P.useRef(!1);return b2(()=>{n.current=!0}),P.useCallback(async(i,s={})=>{zi(n.current,y2),n.current&&(typeof i=="number"?e.navigate(i):await e.navigate(i,{fromRouteId:t,...s}))},[e,t])}var J1={};function w2(e,t,n){!t&&!J1[e]&&(J1[e]=!0,zi(!1,n))}P.memo(f5);function f5({routes:e,future:t,state:n}){return x2(e,void 0,n,t)}function d5({to:e,replace:t,state:n,relative:r}){hn(af()," may be used only in the context of a component.");let{static:i}=P.useContext(Gi);zi(!i," must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.");let{matches:s}=P.useContext(ro),{pathname:u}=xa(),f=Zm(),h=ky(e,Cy(s),u,r==="path"),p=JSON.stringify(h);return P.useEffect(()=>{f(JSON.parse(p),{replace:t,state:n,relative:r})},[f,p,r,t,n]),null}function Wd(e){hn(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function h5({basename:e="/",children:t=null,location:n,navigationType:r="POP",navigator:i,static:s=!1}){hn(!af(),"You cannot render a inside another . You should never have more than one in your app.");let u=e.replace(/^\/*/,"/"),f=P.useMemo(()=>({basename:u,navigator:i,static:s,future:{}}),[u,i,s]);typeof n=="string"&&(n=iu(n));let{pathname:h="/",search:p="",hash:g="",state:b=null,key:y="default"}=n,w=P.useMemo(()=>{let _=Us(h,u);return _==null?null:{location:{pathname:_,search:p,hash:g,state:b,key:y},navigationType:r}},[u,h,p,g,b,y,r]);return zi(w!=null,` is not able to match the URL "${h}${p}${g}" because it does not start with the basename, so the won't render anything.`),w==null?null:P.createElement(Gi.Provider,{value:f},P.createElement(ch.Provider,{children:t,value:w}))}function p5({children:e,location:t}){return J3(G0(e),t)}function G0(e,t=[]){let n=[];return P.Children.forEach(e,(r,i)=>{if(!P.isValidElement(r))return;let s=[...t,i];if(r.type===P.Fragment){n.push.apply(n,G0(r.props.children,s));return}hn(r.type===Wd,`[${typeof r.type=="string"?r.type:r.type.name}] is not a component. All component children of must be a or `),hn(!r.props.index||!r.props.children,"An index route cannot have child routes.");let u={id:r.props.id||s.join("-"),caseSensitive:r.props.caseSensitive,element:r.props.element,Component:r.props.Component,index:r.props.index,path:r.props.path,loader:r.props.loader,action:r.props.action,hydrateFallbackElement:r.props.hydrateFallbackElement,HydrateFallback:r.props.HydrateFallback,errorElement:r.props.errorElement,ErrorBoundary:r.props.ErrorBoundary,hasErrorBoundary:r.props.hasErrorBoundary===!0||r.props.ErrorBoundary!=null||r.props.errorElement!=null,shouldRevalidate:r.props.shouldRevalidate,handle:r.props.handle,lazy:r.props.lazy};r.props.children&&(u.children=G0(r.props.children,s)),n.push(u)}),n}var pm="get",mm="application/x-www-form-urlencoded";function Xm(e){return e!=null&&typeof e.tagName=="string"}function m5(e){return Xm(e)&&e.tagName.toLowerCase()==="button"}function g5(e){return Xm(e)&&e.tagName.toLowerCase()==="form"}function v5(e){return Xm(e)&&e.tagName.toLowerCase()==="input"}function y5(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function b5(e,t){return e.button===0&&(!t||t==="_self")&&!y5(e)}var zp=null;function x5(){if(zp===null)try{new FormData(document.createElement("form"),0),zp=!1}catch{zp=!0}return zp}var w5=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function hv(e){return e!=null&&!w5.has(e)?(zi(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${mm}"`),null):e}function _5(e,t){let n,r,i,s,u;if(g5(e)){let f=e.getAttribute("action");r=f?Us(f,t):null,n=e.getAttribute("method")||pm,i=hv(e.getAttribute("enctype"))||mm,s=new FormData(e)}else if(m5(e)||v5(e)&&(e.type==="submit"||e.type==="image")){let f=e.form;if(f==null)throw new Error('Cannot submit a +

+ + ); +}; + +export default DropInput; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..39b4fda --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,38 @@ +import React, { FC } from "react"; +import { FaGithub } from "react-icons/fa"; +import { Link } from "react-router-dom"; + +import Matomo from "./Matomo"; + +const Footer: FC = () => ( + <> +
+ + Retina logo + +
+ DeepGit is a joint effort between {" "} + + Data Exploration Lab + + {" "} and {" "} + + MOSS + {" "} +
+
+ + + +
+
+ + +); + +export default Footer; diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx new file mode 100644 index 0000000..e76eee6 --- /dev/null +++ b/src/components/Loader.tsx @@ -0,0 +1,17 @@ +import { FC } from "react"; + +export const Loader: FC = () => { + return ( +
+ Loading... +
+ ); +}; + +export const LoaderFill: FC = () => { + return ( +
+ +
+ ); +}; diff --git a/src/components/Matomo.tsx b/src/components/Matomo.tsx new file mode 100644 index 0000000..f7230a3 --- /dev/null +++ b/src/components/Matomo.tsx @@ -0,0 +1,31 @@ +import React, { useContext } from "react"; +import { useLocation } from "react-router-dom"; + +import { GraphContext } from "../lib/context"; + +const matomoUrl: string | undefined = import.meta.env.MATOMO_URL; +const matomoSiteId: string | undefined = import.meta.env.MATOMO_SITE_ID; + +const Matomo: React.FC = () => { + const location = useLocation(); + const context = useContext(GraphContext); + + const url = context?.navState?.url || "local"; + + return ( + <> + {matomoUrl && matomoSiteId && ( + + )} + + ); +}; + +export default Matomo; diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx new file mode 100644 index 0000000..afd659c --- /dev/null +++ b/src/components/Modal.tsx @@ -0,0 +1,81 @@ +import cx from "classnames"; +import { FC } from "react"; +import { createPortal } from "react-dom"; + +import { AppContext } from "../lib/context"; + +interface Props { + title?: string | JSX.Element; + onClose?: () => void; + showHeader?: boolean; + footerAlignLeft?: boolean; + className?: string; + bodyClassName?: string; + children: JSX.Element | [JSX.Element] | [JSX.Element, JSX.Element]; +} + +const UnmountedModal: FC = ({ + onClose, + title, + children, + showHeader = true, + footerAlignLeft = false, + className, + bodyClassName, +}) => { + const childrenArray = Array.isArray(children) ? children : [children]; + const body = childrenArray[0]; + const footer = childrenArray[1]; + + return ( +
+
onClose && onClose()} /> +
+
+ {showHeader && ( +
+ {title && ( +
+ {title} +
+ )} + +
+ )} + {body && ( + + )} + {footer && ( +
+ {footer} +
+ )} +
+
+
+ ); +}; + +const Modal: FC = ({ children, ...props }) => ( + + {(context) => createPortal({children}, context.portalTarget)} + +); + +export default Modal; diff --git a/src/components/Node.tsx b/src/components/Node.tsx new file mode 100644 index 0000000..98f8318 --- /dev/null +++ b/src/components/Node.tsx @@ -0,0 +1,51 @@ +import cx from "classnames"; +import React, { FC, useContext } from "react"; +import { Link } from "react-router-dom"; + +import { GraphContext } from "../lib/context"; +import { NodeData } from "../lib/data"; +import { navStateToQueryURL } from "../lib/navState"; + +const Node: FC<{ + node: string; + attributes: NodeData; + link?: boolean; + className?: string; +}> = ({ node, attributes, link, className }) => { + const { + navState, + setHovered, + computedData: { filteredNodes }, + } = useContext(GraphContext); + const baseClassName = "node fs-6 d-flex flex-row align-items-center"; + + const content = ( + <> + + {attributes.label} + + ); + + return link ? ( + setHovered(node)} + onMouseLeave={() => setHovered(undefined)} + to={"/graph/?" + navStateToQueryURL({ ...navState, selectedNode: node })} + title={attributes.label || undefined} + > + {content} + + ) : ( +
+ {content} +
+ ); +}; + +export default Node; diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..19be6f3 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; + +import { AppContext } from "./lib/context"; +import "./styles/index.scss"; +import Root from "./views/Root"; + +createRoot(document.getElementById("root") as HTMLElement).render( + + + + + , +); diff --git a/src/lib/computedData.ts b/src/lib/computedData.ts new file mode 100644 index 0000000..c0e92c7 --- /dev/null +++ b/src/lib/computedData.ts @@ -0,0 +1,300 @@ +import chroma from "chroma-js"; +import _, { max, min, sortBy } from "lodash"; +import { Dimensions } from "sigma/types"; + +import { findRanges } from "../utils/number"; +import { + DEFAULT_NODE_COLOR, + DEFAULT_NODE_SIZE_RATIO, + EDGE_SIZE_MAX, + EDGE_SIZE_MIN, + GRADIENT, + MAX_PALETTE_SIZE, + NODE_DEFAULT_SIZE, + NODE_SIZE_MAX, + NODE_SIZE_MIN, + PALETTES, +} from "./consts"; +import { Data, countRanges, countTerms, filterNodes, getFilterableFields, getValue } from "./data"; +import { NavState } from "./navState"; + +export interface TermsValue { + id: string; + label: string; + globalCount: number; + filteredCount: number; +} +export interface RangeValue { + min: number; + max: number; + label: string; + globalCount: number; + filteredCount: number; +} +export interface TermsMetric { + type: "quali"; + field: string; + values: TermsValue[]; +} +export interface RangeMetric { + type: "quanti"; + field: string; + unit: number; + min: number; + max: number; + ranges: RangeValue[]; +} +export interface SearchMetrics { + type: "content"; + field: string; + samples: string[]; +} +export type Metric = TermsMetric | RangeMetric | SearchMetrics; +export interface ComputedData { + filteredNodes?: Set | null; // Only present when there are filters + filteredEdges?: Set | null; // Only present when there are filters + metrics: Record; + + // Color and size providers: + // Only present when there is a selected color field + nodeColors?: Record | null; + getColor?: ((value: any) => string) | null; + // Only present when there is a selected size field + getSize?: ((value: any) => number) | null; + nodeSizes: Record; + edgeSizes: Record; + nodeSizeExtents: [number, number]; + edgeSizeExtents: [number, number]; +} + +export function getEmptyComputedData(): ComputedData { + return { + metrics: {}, + nodeSizes: {}, + edgeSizes: {}, + nodeSizeExtents: [0, Infinity], + edgeSizeExtents: [0, Infinity], + }; +} + +export function getNodeColors( + { graph, fieldsIndex }: Data, + { nodeColorField }: Pick, +): Pick { + const result: Pick = {}; + + if (typeof nodeColorField === "string") { + result.nodeColors = {}; + const field = fieldsIndex[nodeColorField]; + let getColor: ComputedData["getColor"] = null; + + if (field.type === "quali") { + const values = sortBy(field.values, (v) => -v.count); + const palette = PALETTES[Math.min(values.length, MAX_PALETTE_SIZE)]; + const colorsDict: Record = values.reduce( + (iter, v, i) => ({ ...iter, [v.id]: palette[i] || DEFAULT_NODE_COLOR }), + {}, + ); + getColor = (value: any) => colorsDict[value] || DEFAULT_NODE_COLOR; + } else if (field.type === "quanti") { + const gradient = chroma.scale(GRADIENT).domain([0, 1]); + getColor = (value: any) => + typeof value === "number" ? gradient((value - field.min) / (field.max - field.min)).hex() : DEFAULT_NODE_COLOR; + } + + if (getColor) { + graph.forEachNode((node, nodeData) => { + result.nodeColors![node] = getColor!(getValue(nodeData, field)); + }); + + result.getColor = getColor; + } + } + + return result; +} + +export function getNodeSizes( + { graph, fieldsIndex }: Data, + { nodeSizeField, nodeSizeRatio }: NavState, + { width, height }: Dimensions, +): Pick { + let nodeSizes: ComputedData["nodeSizes"]; + let getSize: ComputedData["getSize"] = null; + let nodeSizeExtents: ComputedData["nodeSizeExtents"] = [0, Infinity]; + + const ratio = nodeSizeRatio || DEFAULT_NODE_SIZE_RATIO; + const screenSizeRatio = Math.min(width, height) / 1000; + const graphSizeRatio = 1 / Math.log10(graph.order + 2); + + if (typeof nodeSizeField === "string") { + nodeSizes = {}; + const field = fieldsIndex[nodeSizeField]; + + if (field.type === "quanti") { + getSize = (value: any) => { + const size = + typeof value === "number" + ? ((NODE_SIZE_MAX - NODE_SIZE_MIN) * (value - field.min)) / (field.max - field.min) + NODE_SIZE_MIN + : NODE_DEFAULT_SIZE; + return size * ratio * screenSizeRatio * graphSizeRatio; + }; + graph.forEachNode((node, nodeData) => { + nodeSizes![node] = getSize!(getValue(nodeData, field)); + }); + nodeSizeExtents = [field.min, field.max]; + } + } else { + nodeSizes = {}; + const values = graph.mapNodes((_node, attributes) => attributes.rawSize); + nodeSizeExtents = [min(values) as number, max(values) as number]; + graph.forEachNode((node, { rawSize }) => { + nodeSizes[node] = + (((NODE_SIZE_MAX - NODE_SIZE_MIN) * (rawSize - nodeSizeExtents[0])) / + (nodeSizeExtents[1] - nodeSizeExtents[0]) + + NODE_SIZE_MIN) * + ratio * + screenSizeRatio * + graphSizeRatio; + }); + } + + if (nodeSizeExtents[0] === nodeSizeExtents[1]) nodeSizeExtents[0] = 0; + + return { getSize, nodeSizes, nodeSizeExtents }; +} + +export function getEdgeSizes( + { graph, edgesSizeField }: Data, + { edgeSizeRatio }: NavState, + { width, height }: Dimensions, +): Pick { + const ratio = edgeSizeRatio || DEFAULT_NODE_SIZE_RATIO; + const screenSizeRatio = Math.min(width, height) / 1000; + const graphSizeRatio = 1 / Math.log10(graph.order + 2); + + const values = graph.mapEdges((_edge, { attributes }) => attributes[edgesSizeField]); + const edgeSizeExtents: ComputedData["edgeSizeExtents"] = [min(values) as number, max(values) as number]; + if (edgeSizeExtents[0] === edgeSizeExtents[1]) edgeSizeExtents[0] = 0; + + const edgeSizes: ComputedData["edgeSizes"] = {}; + graph.forEachEdge((edge, { attributes }) => { + edgeSizes[edge] = + (((EDGE_SIZE_MAX - EDGE_SIZE_MIN) * ((attributes[edgesSizeField] || edgeSizeExtents[0]) - edgeSizeExtents[0])) / + (edgeSizeExtents[1] - edgeSizeExtents[0]) + + EDGE_SIZE_MIN) * + ratio * + screenSizeRatio * + graphSizeRatio; + }); + + if (edgeSizeExtents[0] === edgeSizeExtents[1]) edgeSizeExtents[0] = 0; + + return { edgeSizes, edgeSizeExtents }; +} + +export function getMetrics( + data: Data, + navState: Pick, + currentMetrics?: ComputedData["metrics"], +): Pick { + const { graph } = data; + + const allFilterable = getFilterableFields(data, navState); + + currentMetrics = currentMetrics || {}; + const metrics: ComputedData["metrics"] = {}; + + // 1. Filter nodes and edges: + const nodes = filterNodes(data, navState); + const nodesArray = nodes ? Array.from(nodes) : null; + const filteredNodes = nodes; + const filteredEdges = nodes + ? new Set(graph.filterEdges((_edge, _attributes, source, target) => nodes.has(source) && nodes.has(target))) + : null; + + // 2. Count metrics: + allFilterable?.forEach((field) => { + const oldMetric = currentMetrics![field.id]; + + switch (field.type) { + case "quali": { + const globalCounts: Record = oldMetric + ? (oldMetric as TermsMetric).values.reduce((iter, v) => ({ ...iter, [v.id]: v.globalCount }), {}) + : countTerms(graph, field); + const counts = nodesArray ? countTerms(graph, field, nodesArray) : globalCounts; + metrics[field.id] = { + type: "quali", + field: field.id, + values: sortBy( + Object.values(field.values).map((value) => ({ + id: value.id, + label: value.label, + globalCount: globalCounts[value.id] || 0, + filteredCount: counts[value.id] || 0, + })), + (o) => -o.globalCount, + ), + }; + break; + } + case "quanti": { + if (oldMetric) { + const { unit, ranges, min, max } = oldMetric as RangeMetric; + const newRanges = ranges.map((v) => [v.min, v.max] as [number, number]); + const counts = nodesArray ? countRanges(graph, field, newRanges, nodesArray) : null; + metrics[field.id] = { + type: "quanti", + field: field.id, + unit, + min, + max, + ranges: ranges.map((v, i) => ({ + ...v, + filteredCount: (counts ? counts[i] : v.globalCount) || 0, + })), + }; + } else { + const { ranges, unit } = findRanges(field.min, field.max); + const globalCounts = countRanges(graph, field, ranges); + const counts = nodesArray ? countRanges(graph, field, ranges, nodesArray) : globalCounts; + const values = graph + .mapNodes((_n, nodeData) => getValue(nodeData, field)) + .filter((v) => typeof v === "number"); + + if (values.length) + metrics[field.id] = { + type: "quanti", + field: field.id, + unit, + min: min(values) as number, + max: max(values) as number, + ranges: ranges.map(([min, max], i) => ({ + min, + max, + label: `${min} - ${max}`, + globalCount: globalCounts[i], + filteredCount: counts[i], + })), + }; + } + break; + } + case "content": { + metrics[field.id] = oldMetric || { + type: "content", + field: field.id, + samples: _(graph.nodes()) + .map((node) => getValue(graph.getNodeAttributes(node), field)) + .filter((str) => !!str) + .uniq() + .take(3) + .value(), + }; + break; + } + } + }); + + return { metrics, filteredNodes, filteredEdges }; +} diff --git a/src/lib/consts.ts b/src/lib/consts.ts new file mode 100644 index 0000000..b756991 --- /dev/null +++ b/src/lib/consts.ts @@ -0,0 +1,118 @@ +import { createNodeBorderProgram } from "@sigma/node-border"; +import { Attributes } from "graphology-types"; +import RAW_PALETTES from "iwanthue/precomputed/k-means-fancy-light"; +import { createElement } from "react"; +import { Props as LinkifyProps } from "react-linkify"; +import { NodeCircleProgram } from "sigma/rendering"; +import { Settings } from "sigma/settings"; + +export const SAMPLE_DATASET_URI = import.meta.env.BASE_URL + "/dataset.gexf"; + +// Palettes +export const PALETTES = RAW_PALETTES as Record; +export const MAX_PALETTE_SIZE = Math.max(...Object.keys(PALETTES).map((s) => +s)); +export const GRADIENT = ["#99f3cb", "#222123"]; + +// Graph rendering +export const NODE_DEFAULT_SIZE = 5; +export const NODE_SIZE_MIN = 10; +export const NODE_SIZE_MAX = 50; + +export const EDGE_DEFAULT_SIZE = 3; +export const EDGE_SIZE_MIN = 1; +export const EDGE_SIZE_MAX = 5; +export const HIGHLIGHTED_EDGE_SIZE_RATIO = 2; + +export const DEFAULT_NODE_COLOR = "#aaa"; +export const DEFAULT_EDGE_COLOR = "#ccc"; +export const HIDDEN_NODE_COLOR = "#f0f0f0"; +export const HIGHLIGHTED_NODE_COLOR = "#333333"; +export const HIDDEN_EDGE_COLOR = "#f6f6f6"; + +export const MIN_NODE_SIZE_RATIO = 0.1; +export const MAX_NODE_SIZE_RATIO = 10; +export const DEFAULT_NODE_SIZE_RATIO = 1; +export const NODE_SIZE_RATIO_STEP = 0.001; + +export const MIN_EDGE_SIZE_RATIO = 0.1; +export const MAX_EDGE_SIZE_RATIO = 10; +export const DEFAULT_EDGE_SIZE_RATIO = 1; +export const EDGE_SIZE_RATIO_STEP = 0.001; + +export const MIN_LABEL_SIZE = 5; +export const MAX_LABEL_SIZE = 50; +export const DEFAULT_LABEL_SIZE = 14; +export const LABEL_SIZE_STEP = 1; + +export const MIN_LABEL_THRESHOLD = 0.1; +export const MAX_LABEL_THRESHOLD = 10; +export const DEFAULT_LABEL_THRESHOLD = 1; +export const LABEL_THRESHOLD_STEP = 0.001; + +export const ANIMATION_DURATION = 400; +export const MAX_OPTIONS = 50; + +export const BASE_SIGMA_SETTINGS: Partial = { + labelFont: '"Public Sans", sans-serif', + allowInvalidContainer: true, + zIndex: true, + nodeReducer: hiddenReducer, + edgeReducer: hiddenReducer, + defaultNodeType: "circle", + nodeProgramClasses: { + circle: NodeCircleProgram, + bordered: createNodeBorderProgram({ + borders: [ + { size: { value: 0.2 }, color: { attribute: "borderColor" } }, + { size: { fill: true }, color: { attribute: "color" } }, + ], + }), + }, +}; + +// Data indexation +export const RESERVED_FIELDS = new Set(["label", "size", "color", "x", "y", "z"]); + +export const RETINA_FIELD_PREFIX = "RETINA::"; +export const RETINA_HIDDEN_FIELD_PREFIX = RETINA_FIELD_PREFIX + "HIDDEN::"; +export const RETINA_NUMBER_FIELD_PREFIX = RETINA_FIELD_PREFIX + "NUMBER::"; +export const RETINA_STRING_FIELD_PREFIX = RETINA_FIELD_PREFIX + "STRING::"; + +export function isHiddenRetinaField(str: string): boolean { + return str.indexOf(RETINA_HIDDEN_FIELD_PREFIX) === 0; +} + +export function removeRetinaPrefix(str: string): string { + return str.replace(RETINA_NUMBER_FIELD_PREFIX, "").replace(RETINA_STRING_FIELD_PREFIX, ""); +} + +export function hiddenReducer(_key: string, data: Attributes) { + return { ...data, hidden: true }; +} + +// Vendor component styles +export const SLIDER_STYLE = { + dotStyle: { borderColor: "#ccc" }, + railStyle: { backgroundColor: "#ccc" }, + activeDotStyle: { borderColor: "black" }, + trackStyle: { backgroundColor: "black" }, + handleStyle: { backgroundColor: "white", borderColor: "black" }, +}; +export const RANGE_STYLE = { + dotStyle: { borderColor: "#ccc" }, + railStyle: { backgroundColor: "#ccc" }, + activeDotStyle: { borderColor: "black" }, + trackStyle: [{ backgroundColor: "black" }, { backgroundColor: "black" }], + handleStyle: [ + { backgroundColor: "white", borderColor: "black" }, + { backgroundColor: "white", borderColor: "black" }, + ], +}; +export const DEFAULT_SELECT_PROPS = { + classNamePrefix: "react-select", +}; +export const DEFAULT_LINKIFY_PROPS: Partial = { + textDecorator: (str) => str.replace(/^https?:\/\//, ""), + componentDecorator: (decoratedHref: string, decoratedText: string, key: number) => + createElement("a", { key, href: decoratedHref, target: "_blank", rel: "noreferrer", decoratedText }), +}; diff --git a/src/lib/context.ts b/src/lib/context.ts new file mode 100644 index 0000000..7118a00 --- /dev/null +++ b/src/lib/context.ts @@ -0,0 +1,49 @@ +import { createContext } from "react"; +import Sigma from "sigma"; + +import { ModalName } from "../views/modals"; +import { ComputedData } from "./computedData"; +import { Data } from "./data"; +import { NavState } from "./navState"; + +export const PANELS = ["main", "readability"] as const; +export type Panel = (typeof PANELS)[number]; + +export const AppContext = createContext<{ portalTarget: HTMLDivElement }>({ + portalTarget: document.createElement("div"), +}); + +type GraphContextType = { + embedMode: boolean; + data: Data; + graphFile: { + name: string; + extension: string; + textContent: string; + }; + + isPanelExpanded: boolean; + setIsPanelExpanded: (isPanelExpanded: boolean) => void; + + navState: NavState; + computedData: ComputedData; + hovered: string | Set | undefined; + + setNavState: (newNavState: NavState) => void; + setHovered: (hovered?: string | Set) => void; + + panel: Panel; + setPanel: (panel: Panel) => void; + + modal: ModalName | undefined; + openModal: (modal: ModalName) => void; + closeModal: () => void; + + sigma: Sigma | undefined; + setSigma: (sigma: Sigma | undefined) => void; + root: HTMLElement | undefined; +}; +export const GraphContext = createContext( + // "Fake" initial value (proper value will be given by Provider) + null as unknown as GraphContextType, +); diff --git a/src/lib/data.ts b/src/lib/data.ts new file mode 100644 index 0000000..31728de --- /dev/null +++ b/src/lib/data.ts @@ -0,0 +1,508 @@ +import { MultiGraph } from "graphology"; +import gexf from "graphology-gexf/browser"; +import graphml from "graphology-graphml/browser"; +import forceAtlas2 from "graphology-layout-forceatlas2"; +import noverlap from "graphology-layout-noverlap"; +import circular from "graphology-layout/circular"; +import AbstractGraph from "graphology-types"; +import { constant, flatMap, groupBy, isNil, keyBy, mapValues, max, min, omitBy, uniq } from "lodash"; +import { NodeDisplayData } from "sigma/types"; + +import { isNumber } from "../utils/number"; +import { minimize, normalize } from "../utils/string"; +import { + DEFAULT_EDGE_COLOR, + DEFAULT_NODE_COLOR, + NODE_DEFAULT_SIZE, + RESERVED_FIELDS, + RETINA_FIELD_PREFIX, + RETINA_NUMBER_FIELD_PREFIX, + RETINA_STRING_FIELD_PREFIX, + removeRetinaPrefix, +} from "./consts"; +import { BAD_EXTENSION } from "./errors"; +import { FILTER_FIELD_TYPES, Filter, NavState } from "./navState"; + +/** + * Types: + * ****** + */ +export interface BaseField { + type: string; + label: string; + typeLabel?: string; + computed?: boolean; + id: string; + rawFieldId: string; + nullValuesCount: number; +} +export interface ContentField extends BaseField { + type: "content"; +} +export interface QuantiField extends BaseField { + type: "quanti"; + min: number; + max: number; +} +export interface QualiField extends BaseField { + type: "quali"; + values: Record< + string, + { + id: string; + label: string; + count: number; + } + >; +} +export type Field = QualiField | QuantiField | ContentField; +export type FieldType = Field["type"]; +export type TypedField = Partial<{ + content: ContentField; + quanti: QuantiField; + quali: QualiField; +}>; + +export type CustomNodeDisplayData = NodeDisplayData & { trueColor: string }; + +export type RawData = Record; + +export interface NodeData { + x: number; + y: number; + label: string; + size: number; + rawSize: number; // size from graph file or default size + color: string; + rawColor: string; // color from graph file or default color + + // For render only: + italic?: true; // for missing labels + + labelSize: number; + subtitles: string[]; + + // Everything computed and cached by Retina: + computed: { + degree: number; + }; + + // Everything from the graph file: + attributes: RawData; +} + +export interface EdgeData { + size: number; + rawSize: number; // size from graph file or default size + color: string; + label?: string; + rawColor: string; // color from graph file or default color + type?: "arrow"; + + directed?: boolean; // `undefined` if not determined + + // Optional state: + hidden?: boolean; + + // Everything from the graph file: + attributes: RawData; +} + +export type RawGraph = AbstractGraph; +export type RetinaGraph = MultiGraph; + +export interface Data { + graph: RetinaGraph; + fields: string[]; // A (sorted) array of field keys + fieldsIndex: Record; // The actual field contents + edgeFields: string[]; + edgeFieldsIndex: Record; + // TODO: + // - Move that edgeSizeField value into navState + // - Add an input to select it + edgesSizeField: string; +} + +export interface Report { + missingNodeSizes?: number; + missingNodeColors?: number; + missingNodeLabels?: number; + missingNodePositions?: number; + missingEdgeSizes?: number; + missingEdgeColors?: number; +} + +/** + * Loading graph: + * ************** + */ +export const Loaders: { [extension: string]: (text: string) => RawGraph } = { + gexf: (text) => gexf.parse(MultiGraph, text, { addMissingNodes: true }), + graphml: (text) => graphml.parse(MultiGraph, text, { addMissingNodes: true }), +}; +export async function loadGraphURL(path: string): Promise<{ name: string; extension: string; textContent: string }> { + const name = (path.split("/").pop() || "").toLowerCase(); + const extension = (name.split(".").pop() || "").toLowerCase(); + const textContent = await fetch(path).then((res) => res.text()); + + return { name, extension, textContent }; +} +export async function loadGraphFile(file: File): Promise<{ name: string; extension: string; textContent: string }> { + const name = file.name; + const extension = (name.split(".").pop() || "").toLowerCase(); + const textContent = await file.text(); + + return { name, extension, textContent }; +} +export async function readGraph({ + extension, + textContent, +}: { + name: string; + extension: string; + textContent: string; +}): Promise { + if (!Loaders[extension]) { + const e = new Error(`Graph file extension ".${extension}" not recognized.`); + e.name = BAD_EXTENSION; + throw e; + } + + return Loaders[extension](textContent); +} + +export function prepareGraph(rawGraph: RawGraph): { graph: RetinaGraph; report: Report } { + const graph = new MultiGraph(); + const report: Report = {}; + + rawGraph.forEachNode((node, attributes) => { + const { x, y, size, color, label } = attributes; + + if (typeof attributes.x !== "number" || typeof attributes.y !== "number") + report.missingNodePositions = (report.missingNodePositions || 0) + 1; + if (typeof attributes.size !== "number") report.missingNodeSizes = (report.missingNodeSizes || 0) + 1; + if (typeof attributes.color !== "string") report.missingNodeColors = (report.missingNodeColors || 0) + 1; + if (typeof attributes.label !== "string") report.missingNodeLabels = (report.missingNodeLabels || 0) + 1; + + const newNodeAttributes: NodeData = { + x: typeof x === "number" ? x : 0, + y: typeof y === "number" ? y : 0, + label: typeof label === "string" ? label : node, + size: typeof size === "number" ? size : NODE_DEFAULT_SIZE, + rawSize: typeof size === "number" ? size : NODE_DEFAULT_SIZE, + color: typeof color === "string" ? color : DEFAULT_NODE_COLOR, + rawColor: typeof color === "string" ? color : DEFAULT_NODE_COLOR, + + italic: typeof label !== "string" || undefined, + + labelSize: NODE_DEFAULT_SIZE, + subtitles: [], + + attributes, + + computed: { + degree: NaN, + }, + }; + + graph.addNode(node, newNodeAttributes); + }); + rawGraph.forEachEdge((edge, attributes, source, target) => { + const { size, color, label } = attributes; + const directed = rawGraph.isDirected(edge); + + if (typeof attributes.size !== "number") report.missingEdgeSizes = (report.missingEdgeSizes || 0) + 1; + if (typeof attributes.color !== "string") report.missingEdgeColors = (report.missingEdgeColors || 0) + 1; + + const newEdgeAttributes: EdgeData = { + size: typeof size === "number" ? size : NODE_DEFAULT_SIZE, + rawSize: typeof size === "number" ? size : NODE_DEFAULT_SIZE, + color: typeof color === "string" ? color : DEFAULT_EDGE_COLOR, + rawColor: typeof color === "string" ? color : DEFAULT_EDGE_COLOR, + label: typeof label === "string" ? label : undefined, + directed, + + hidden: false, + type: undefined, + + attributes, + }; + + if (directed) graph.addDirectedEdgeWithKey(edge, source, target, newEdgeAttributes); + else graph.addUndirectedEdgeWithKey(edge, source, target, newEdgeAttributes); + }); + + // For positions however, we need to run FA2 for some time first: + // TODO: Try running this in a worker instead + if (report.missingNodePositions) { + circular.assign(graph); + + forceAtlas2.assign(graph, { + settings: forceAtlas2.inferSettings(graph), + iterations: 150, + }); + + noverlap.assign(graph, { maxIterations: 150 }); + } + + return { graph, report }; +} + +/** + * Initializing graph data: + * ************************ + */ +export function inferFieldTypes(values: (string | number)[], nodesCount: number): FieldType[] { + const types: FieldType[] = []; + + if (values.every((v) => isNumber(v))) types.push("quanti"); + + const uniqValuesCount = uniq(values).length; + if (uniqValuesCount > 1 && uniqValuesCount < Math.max(Math.pow(nodesCount, 0.75), 5)) types.push("quali"); + if (!types.length) types.push("content"); + + return types; +} + +export function getValue(node: NodeData, field: Field): any { + return field.computed ? (node.computed as any)[field.rawFieldId] : node.attributes[field.rawFieldId]; +} + +export function getFields(graph: RetinaGraph, type: "node" | "edge"): Field[] { + let fields: Record = {}; + + // Inject computed fields (here, only "Degree" for now): + const computedFields: Field[] = []; + if (type === "node") { + const degreeExtent: [number, number] = [Infinity, -Infinity]; + graph.forEachNode((node) => { + graph.updateNodeAttribute(node, "computed", (o) => { + const degree = graph.degree(node); + degreeExtent[0] = Math.min(degreeExtent[0], degree); + degreeExtent[1] = Math.max(degreeExtent[1], degree); + return { ...o, degree }; + }); + }); + computedFields.push({ + type: "quanti", + id: RETINA_FIELD_PREFIX + "degree", + computed: true, + rawFieldId: "degree", + label: "Degree", + min: degreeExtent[0], + max: degreeExtent[1], + nullValuesCount: 0, + }); + } + + // Identify all values: + graph[type === "node" ? "forEachNode" : "forEachEdge"]((_, { attributes }) => { + for (const key in attributes) { + const value = attributes[key]; + if (!isNil(value)) { + if (!fields[key]) fields[key] = []; + fields[key].push(attributes[key]); + } + } + }); + + // Remove reserved fields: + fields = omitBy(fields, (_, field) => RESERVED_FIELDS.has(field)); + + // Minimize field IDs: + const keys = Object.keys(fields).concat(computedFields.map((o) => o.id)); + const minimized: Record = minimize( + keys.map(removeRetinaPrefix).map((s) => normalize(s || "").trim()), + ).reduce( + (iter, mini, i) => ({ + ...iter, + [keys[i]]: mini, + }), + {}, + ); + + // Update computed fields IDs: + computedFields.forEach((field) => (field.id = minimized[field.id])); + + // Infer field types: + const totalRowsCount = type === "node" ? graph.order : graph.size; + const inferedFields: Field[] = flatMap(fields, (values, key) => { + const types = + key.indexOf(RETINA_NUMBER_FIELD_PREFIX) === 0 + ? (["quanti"] as FieldType[]) + : key.indexOf(RETINA_STRING_FIELD_PREFIX) === 0 + ? (["quali"] as FieldType[]) + : inferFieldTypes(values, totalRowsCount); + + return types.map((type) => { + const id = minimized[key]; + const label = removeRetinaPrefix(key); + + switch (type) { + case "quali": + return { + type, + id: types.length > 1 ? `${id}-s` : id, + rawFieldId: key, + label, + typeLabel: types.length > 1 ? "as qualitative values" : undefined, + nullValuesCount: totalRowsCount - values.length, + values: mapValues(groupBy(values), (a, v) => ({ + id: v, + label: v, + count: a.length, + })), + }; + case "quanti": { + const numbers = values.filter((v) => isNumber(v)).map((v) => +v) as number[]; + return { + type, + id: types.length > 1 ? `${id}-n` : id, + rawFieldId: key, + label, + typeLabel: types.length > 1 ? "as quantitative values" : undefined, + nullValuesCount: totalRowsCount - values.length, + min: min(numbers) as number, + max: max(numbers) as number, + }; + } + case "content": + default: + return { + type: "content", + id: types.length > 1 ? `${id}-t` : id, + rawFieldId: key, + label, + nullValuesCount: totalRowsCount - values.length, + typeLabel: types.length > 1 ? "as searchable text" : undefined, + }; + } + }); + }); + + return inferedFields.concat(computedFields); +} + +export function enrichData(graph: RetinaGraph): Data { + const fields = getFields(graph, "node"); + const edgeFields = getFields(graph, "edge"); + + // Reindex number fields as numbers: + const fieldsToReindex = uniq( + fields.filter((field) => field.type === "quanti" && !field.computed).map((field) => field.rawFieldId), + ); + if (fieldsToReindex.length) { + graph.forEachNode((node) => { + graph.updateNodeAttribute(node, "attributes", (attributes = {}) => ({ + ...attributes, + ...fieldsToReindex.reduce((iter, key) => ({ ...iter, [key]: +attributes[key] }), {}), + })); + }); + } + const edgeFieldsToReindex = uniq( + edgeFields.filter((field) => field.type === "quanti" && !field.computed).map((field) => field.rawFieldId), + ); + if (edgeFieldsToReindex.length) { + graph.forEachEdge((edge) => { + graph.updateEdgeAttribute(edge, "attributes", (attributes = {}) => ({ + ...attributes, + ...fieldsToReindex.reduce((iter, key) => ({ ...iter, [key]: +attributes[key] }), {}), + })); + }); + } + + // Guess edge size field: + const ACCEPTABLE_SIZE_FIELDS = new Set(["size", "weight"]); + const edgesSizeField = + edgeFields.find((field) => ACCEPTABLE_SIZE_FIELDS.has(field.rawFieldId.toLowerCase()) && field.type === "quanti") + ?.rawFieldId || "size"; + + return { + graph, + fieldsIndex: keyBy(fields, "id"), + fields: fields.map((field) => field.id), + edgeFieldsIndex: keyBy(edgeFields, "id"), + edgeFields: edgeFields.map((field) => field.id), + edgesSizeField, + }; +} + +/** + * Filtering data: + * *************** + */ +export function countTerms(graph: RetinaGraph, field: Field, nodes?: string[] | null): Record { + const counts: Record = {}; + const nodesToSearch = nodes || graph.nodes(); + + nodesToSearch.forEach((n) => { + const v = getValue(graph.getNodeAttributes(n), field); + if (!isNil(v)) counts[v] = (counts[v] || 0) + 1; + }); + + return counts; +} +export function countRanges( + graph: RetinaGraph, + field: Field, + ranges: [number, number][], + nodes?: string[] | null, +): number[] { + const counts: number[] = ranges.map(constant(0)); + const nodesToSearch = nodes || graph.nodes(); + + nodesToSearch.forEach((n) => { + const v = getValue(graph.getNodeAttributes(n), field); + if (typeof v === "number") { + const matchIndex = ranges.findIndex(([min, max]) => v >= min && v < max); + if (matchIndex >= 0) counts[matchIndex]++; + } + }); + + return counts; +} + +export function filterNode(nodeData: NodeData, filters: Filter[], fieldsIndex: Record): boolean { + return filters.every((filter) => { + const field = fieldsIndex[filter.field]; + if (!field || field.type !== FILTER_FIELD_TYPES[filter.type]) return false; + + const value = getValue(nodeData, field); + + switch (filter.type) { + case "range": + return ( + typeof value === "number" && + (typeof filter.min !== "number" || filter.min <= value) && + (typeof filter.max !== "number" || filter.max > value) + ); + case "terms": + return !isNil(value) && filter.values.includes(value + ""); + case "search": + return value && normalize(value).includes(filter.normalizedValue); + default: + return false; + } + }); +} + +export function filterNodes(data: Data, { filters }: Pick): Set | null { + const { graph, fieldsIndex } = data; + + if (!filters || !filters.length) return null; + return new Set(graph.filterNodes((node, attributes) => filterNode(attributes, filters, fieldsIndex))); +} + +/** + * Various views: + * ************** + */ +export function getFilterableFields( + data: Data, + { filterable, colorable, sizeable }: Pick, +): Field[] { + const { fields, fieldsIndex } = data; + const filterableSet = new Set([...(filterable || []), ...(colorable || []), ...(sizeable || [])]); + + return fields.filter((f) => filterableSet.has(f)).map((f) => fieldsIndex[f]); +} diff --git a/src/lib/errors.tsx b/src/lib/errors.tsx new file mode 100644 index 0000000..18f6856 --- /dev/null +++ b/src/lib/errors.tsx @@ -0,0 +1,117 @@ +import { flatMap, pickBy, sortBy } from "lodash"; + +import { Report } from "./data"; +import { NotificationInput } from "./notifications"; + +/** + * Errors management: + */ +export const MISSING_URL = "missing-url"; +export const MISSING_FILE = "missing-file"; +export const BAD_EXTENSION = "bad-ext"; +export const BAD_URL = "bad-url"; +export const BAD_FILE = "bad-file"; +export const UNKNOWN = "unknown"; + +const ERRORS_DICT: Record = { + [MISSING_URL]: "You need to specify a graph file URL.", + [MISSING_FILE]: "You need to specify a local file or a graph URL to load.", + [BAD_EXTENSION]: "The extension of the given graph file is not recognized.", + [BAD_URL]: "The graph at the given URL could not be loaded.", + [BAD_FILE]: "The graph at the given URL could not be parsed.", + [UNKNOWN]: "An unknown error occurred.", +}; + +export function getErrorMessage(errorType: string): string { + return ERRORS_DICT[errorType] || "Something went wrong."; +} + +/** + * Reports management: + */ +const LEVELS_ORDER = { + error: 0, + warning: 1, + info: 2, +}; + +const REPORT_DICT: Record< + string, + { level: "info" | "warning" | "error"; log: (count: number) => string | JSX.Element } +> = { + missingEdgeSizes: { + level: "info", + log: (n) => `${n === 1 ? "One" : n} edge${n > 1 ? "s have" : " has"} no size.`, + }, + missingEdgeColors: { + level: "info", + log: (n) => `${n === 1 ? "One" : n} edge${n > 1 ? "s have" : " has"} no color.`, + }, + missingNodeSizes: { + level: "info", + log: (n) => `${n === 1 ? "One" : n} node${n > 1 ? "s have" : " has"} no size.`, + }, + missingNodeColors: { + level: "info", + log: (n) => `${n === 1 ? "One" : n} node${n > 1 ? "s have" : " has"} no color. `, + }, + missingNodeLabels: { + level: "warning", + log: (n) => + `${n > 1 ? n : "One"} node${n > 1 ? "s have" : " has"} no label. ${ + n > 1 ? "Their keys are" : "Its key is" + } used instead (${n > 1 ? "they are" : "it is"} italic in the graph).`, + }, + missingNodePositions: { + level: "warning", + log: (n) => ( + <> + {n === 1 ? "One" : n} node{n > 1 ? "s have" : " has"} no position. The layout has been determined using{" "} + + ForceAtlas2 + + . However, it would be better to load a file with the layout already computed. + + ), + }, +}; + +export function getReportNotification(report: Report, skipInfo?: boolean): NotificationInput | null { + const minimalReport: Record = pickBy(report, (val, key) => !!val && !!REPORT_DICT[key]); + const messages = sortBy( + flatMap(minimalReport, (val, key) => { + const log = REPORT_DICT[key].log(val); + const level = REPORT_DICT[key].level; + + if (skipInfo && level === "info") return []; + + return [{ message: log, level }]; + }), + ({ level }) => LEVELS_ORDER[level] || Infinity, + ); + + if (!messages.length) return null; + + const mostImportantLevel = messages[0].level; + + return { + type: mostImportantLevel, + keepAlive: mostImportantLevel !== "info", + message: ( + <> + {messages.length > 1 ? "Some things" : "Something"} to note about the graph dataset: +
    + {messages.map(({ message, level }, i) => ( +
  • + {message} +
  • + ))} +
+ + ), + }; +} diff --git a/src/lib/graph.ts b/src/lib/graph.ts new file mode 100644 index 0000000..7d49392 --- /dev/null +++ b/src/lib/graph.ts @@ -0,0 +1,234 @@ +import chroma from "chroma-js"; +import { Attributes } from "graphology-types"; +import { isNil, isSet, memoize } from "lodash"; +import { Settings } from "sigma/settings"; + +import { ComputedData } from "./computedData"; +import { + DEFAULT_EDGE_COLOR, + DEFAULT_EDGE_SIZE_RATIO, + DEFAULT_LABEL_SIZE, + DEFAULT_NODE_SIZE_RATIO, + HIDDEN_NODE_COLOR, + HIGHLIGHTED_EDGE_SIZE_RATIO, + HIGHLIGHTED_NODE_COLOR, +} from "./consts"; +import { Data, EdgeData, NodeData, getValue } from "./data"; +import { NavState } from "./navState"; + +const getLighterColor = memoize((color: string): string => { + return chroma.average([color, HIDDEN_NODE_COLOR], "lab").hex(); +}); + +export function applyNodeColors({ graph }: Data, { nodeColors }: Pick) { + graph.forEachNode((node, { rawColor }) => + graph.setNodeAttribute(node, "color", nodeColors ? nodeColors[node] : rawColor), + ); +} + +export function applyNodeSizes( + { graph }: Data, + { nodeSizes }: Pick, + { nodeSizeRatio }: Pick, +) { + const ratio = typeof nodeSizeRatio === "number" ? nodeSizeRatio : DEFAULT_NODE_SIZE_RATIO; + graph.forEachNode((node, { rawSize }) => + graph.setNodeAttribute(node, "size", (nodeSizes ? nodeSizes[node] : rawSize) * ratio), + ); +} + +export function applyNodeLabelSizes( + { graph, fieldsIndex }: Data, + { nodeSizeExtents }: Pick, + { nodeSizeField, minLabelSize, maxLabelSize }: Pick, +) { + const minSize = typeof minLabelSize === "number" ? minLabelSize : DEFAULT_LABEL_SIZE; + const maxSize = typeof maxLabelSize === "number" ? maxLabelSize : DEFAULT_LABEL_SIZE; + const extentDelta = nodeSizeExtents[1] - nodeSizeExtents[0]; + const factor = (maxSize - minSize) / (extentDelta || 1); + graph.forEachNode((node, nodeData) => { + const nodeSize = nodeSizeField ? getValue(nodeData, fieldsIndex[nodeSizeField]) : nodeData.rawSize; + graph.setNodeAttribute(node, "labelSize", minSize + (nodeSize - nodeSizeExtents[0]) * factor); + }); +} + +export function applyNodeSubtitles({ graph, fieldsIndex }: Data, { subtitleFields }: Pick) { + graph.forEachNode((node, nodeData) => + graph.setNodeAttribute( + node, + "subtitles", + subtitleFields + ? subtitleFields.flatMap((f) => { + const field = fieldsIndex[f]; + const val = getValue(nodeData, field); + return isNil(val) ? [] : [`${field.label}: ${typeof val === "number" ? val.toLocaleString() : val}`]; + }) + : [], + ), + ); +} + +export function applyEdgeColors( + { graph }: Data, + { nodeColors }: Pick, + { edgeColoring }: Pick, +) { + let getColor: (edge: string, data: EdgeData) => string; + + switch (edgeColoring) { + case "s": + case "t": + getColor = (edge: string) => { + const node = edgeColoring === "s" ? graph.source(edge) : graph.target(edge); + return nodeColors ? nodeColors[node] : graph.getNodeAttribute(node, "rawColor"); + }; + break; + case "c": + getColor = () => DEFAULT_EDGE_COLOR; + break; + case "o": + default: + getColor = (edge, { rawColor }) => rawColor; + } + + graph.forEachEdge((edge, data) => graph.setEdgeAttribute(edge, "color", getColor(edge, data))); +} + +export function applyEdgeDirections({ graph }: Data, { edgeDirection }: Pick) { + let getDirection: (edge: string, data: EdgeData) => boolean | undefined; + + switch (edgeDirection) { + case "d": + getDirection = () => true; + break; + case "u": + getDirection = () => false; + break; + case "o": + default: + getDirection = (edge) => graph.isDirected(edge); + } + + graph.forEachEdge((edge, data) => { + const directed = getDirection(edge, data); + graph.mergeEdgeAttributes(edge, { directed, type: directed ? "arrow" : undefined }); + }); +} + +export function applyEdgeSizes( + { graph }: Data, + { edgeSizes }: Pick, + { edgeSizeRatio }: Pick, +) { + const ratio = typeof edgeSizeRatio === "number" ? edgeSizeRatio : DEFAULT_EDGE_SIZE_RATIO; + graph.forEachEdge((edge, { rawSize }) => + graph.setEdgeAttribute(edge, "size", (edgeSizes ? edgeSizes[edge] : rawSize) * ratio), + ); +} + +export function applyGraphStyle(data: Data, computedData: ComputedData, navState: NavState) { + applyNodeColors(data, computedData); + applyNodeSizes(data, computedData, navState); + applyNodeLabelSizes(data, computedData, navState); + applyNodeSubtitles(data, navState); + applyEdgeColors(data, computedData, navState); + applyEdgeDirections(data, navState); + applyEdgeSizes(data, computedData, navState); +} + +export function getReducers( + dataset: Data, + navState: NavState, + computedData: ComputedData, + hovered: string | Set | undefined, +): { + node: NonNullable; + edge: NonNullable; +} { + const { graph } = dataset; + const { selectedNode } = navState; + const { filteredNodes } = computedData; + + const greyedOutNodes = new Set(); + const emphasizedNodesSet = new Set(); + const highlightedNodesSet = new Set(); + + if (isSet(hovered)) { + if (selectedNode) highlightedNodesSet.add(selectedNode); + + graph.forEachNode((n) => { + if (hovered.has(n)) { + emphasizedNodesSet.add(n); + } else if (n !== selectedNode) { + greyedOutNodes.add(n); + } + }); + } else if (typeof hovered === "string" || selectedNode) { + if (hovered) { + highlightedNodesSet.add(hovered); + emphasizedNodesSet.add(hovered); + } + if (selectedNode) { + highlightedNodesSet.add(selectedNode); + emphasizedNodesSet.add(selectedNode); + } + + const highlightedNodes = Array.from(highlightedNodesSet); + graph.forEachNode((n) => { + if (highlightedNodes.some((highlightedNode) => graph.areNeighbors(n, highlightedNode))) { + emphasizedNodesSet.add(n); + } else if (!highlightedNodesSet.has(n)) { + greyedOutNodes.add(n); + } + }); + } + + return { + node(node: string, anyData: Attributes) { + const data = anyData as NodeData; + const res = { ...anyData }; + + let noLabel = false; + + if (emphasizedNodesSet.has(node)) { + res.color = data.color; + res.borderColor = HIGHLIGHTED_NODE_COLOR; + res.type = "bordered"; + res.zIndex = 1000; + noLabel = false; + } else if (filteredNodes && !filteredNodes.has(node)) { + res.color = HIDDEN_NODE_COLOR; + noLabel = true; + } else if (greyedOutNodes.has(node)) { + res.color = getLighterColor(data.color); + noLabel = true; + } + + if (highlightedNodesSet.has(node)) { + res.highlighted = true; + noLabel = false; + } + + if (noLabel) { + res.hideLabel = true; + res.subtitles = []; + res.zIndex = -1; + } + + return res; + }, + edge(edge: string, data: Attributes) { + const res = { ...data }; + + if (graph.extremities(edge).some((n) => greyedOutNodes.has(n) || (filteredNodes && !filteredNodes.has(n)))) { + res.hidden = true; + } + + if (hovered || selectedNode) { + res.size *= HIGHLIGHTED_EDGE_SIZE_RATIO; + } + + return res; + }, + }; +} diff --git a/src/lib/navState.test.ts b/src/lib/navState.test.ts new file mode 100644 index 0000000..1930012 --- /dev/null +++ b/src/lib/navState.test.ts @@ -0,0 +1,421 @@ +import { MultiGraph } from "graphology"; +import { describe, expect, test } from "vitest"; + +import { normalize } from "../utils/string"; +import { + DEFAULT_LABEL_SIZE, + DEFAULT_LABEL_THRESHOLD, + DEFAULT_NODE_SIZE_RATIO, + MAX_EDGE_SIZE_RATIO, + MAX_LABEL_SIZE, + MAX_LABEL_THRESHOLD, + MAX_NODE_SIZE_RATIO, + MIN_EDGE_SIZE_RATIO, + MIN_LABEL_SIZE, + MIN_LABEL_THRESHOLD, + MIN_NODE_SIZE_RATIO, +} from "./consts"; +import { Data, enrichData, prepareGraph } from "./data"; +import { + DEFAULT_EDGE_COLORING, + DEFAULT_EDGE_DIRECTION, + DEFAULT_ROLE, + Role, + cleanFilter, + cleanNavState, + navStateToQueryURL, + queryURLToNavState, +} from "./navState"; + +function data(): Data { + const rawGraph = new MultiGraph(); + rawGraph.import({ + nodes: [ + { key: "John", attributes: { name: "Doe", age: 34, childrenCount: 2, description: "Lorem ipsum" } }, + { key: "Jack", attributes: { name: "Black", age: 56, childrenCount: 1, description: "Lorem ipsum dolor" } }, + ], + edges: [{ source: "John", target: "Jack" }], + }); + + const { graph } = prepareGraph(rawGraph); + return enrichData(graph); +} + +describe("NavState", () => { + describe("#cleanFilter", () => { + test("should do nothing for valid terms filter", () => { + expect(cleanFilter({ type: "terms", field: "n", values: ["Doe"] }, data())).toStrictEqual({ + type: "terms", + field: "n", + values: ["Doe"], + }); + }); + test("should remove unexisting values from terms filters", () => { + expect(cleanFilter({ type: "terms", field: "n", values: ["Doe", "Obama"] }, data())).toStrictEqual({ + type: "terms", + field: "n", + values: ["Doe"], + }); + }); + test("should return null if no term value exists", () => { + expect(cleanFilter({ type: "terms", field: "n", values: ["Obama"] }, data())).toStrictEqual(null); + }); + test("should return null if the terms field does not exist", () => { + expect(cleanFilter({ type: "terms", field: "firstName", values: ["John"] }, data())).toStrictEqual(null); + }); + test("should return null if the terms field is not a quali field", () => { + expect(cleanFilter({ type: "terms", field: "a-n", values: ["10"] }, data())).toStrictEqual(null); + }); + + test("should do nothing for valid range filter", () => { + expect(cleanFilter({ type: "range", field: "a-n", min: 10 }, data())).toStrictEqual({ + type: "range", + field: "a-n", + min: 10, + }); + }); + test("should return null if there is no min nor no max", () => { + expect(cleanFilter({ type: "range", field: "a-n" }, data())).toStrictEqual(null); + }); + test("should return null if the range field does not exist", () => { + expect(cleanFilter({ type: "range", field: "birthYear", max: 1980 }, data())).toStrictEqual(null); + }); + test("should return null if the range field is not a quanti field", () => { + expect(cleanFilter({ type: "range", field: "n", max: 123 }, data())).toStrictEqual(null); + }); + }); + + describe("#cleanNavState", () => { + test("should do nothing for valid nav state", () => { + expect( + cleanNavState( + { + sizeable: ["a-n"], + colorable: ["n"], + nodeSizeField: "a-n", + nodeColorField: "n", + selectedNode: "John", + disableDefaultSize: true, + disableDefaultColor: true, + edgeColoring: "s", + }, + data(), + ), + ).toStrictEqual({ + sizeable: ["a-n"], + colorable: ["n"], + nodeSizeField: "a-n", + nodeColorField: "n", + selectedNode: "John", + disableDefaultSize: true, + disableDefaultColor: true, + edgeColoring: "s", + }); + }); + + test("should remove `role` if it is not a proper value", () => { + expect(cleanNavState({ role: "woopsy" as Role }, data())).toStrictEqual({}); + }); + test("should remove `role` if it is the default value", () => { + expect(cleanNavState({ role: DEFAULT_ROLE }, data())).toStrictEqual({}); + }); + test("should keep `role` otherwise", () => { + expect(cleanNavState({ role: "x" }, data())).toStrictEqual({ role: "x" }); + expect(cleanNavState({ role: "v" }, data())).toStrictEqual({ role: "v" }); + }); + + test("should remove `selectedNode` if it does not exist in the graph", () => { + expect(cleanNavState({ selectedNode: "nothing" }, data())).toStrictEqual({}); + }); + + test("should keep `subtitleFields` if they are valid", () => { + expect(cleanNavState({ subtitleFields: ["a-n"] }, data())).toStrictEqual({ subtitleFields: ["a-n"] }); + }); + + test("should remove `subtitleFields` if it does not exist in the graph", () => { + expect(cleanNavState({ subtitleFields: ["nothing"] }, data())).toStrictEqual({}); + }); + + test("should remove `color` if it does not exist in the graph", () => { + expect(cleanNavState({ nodeColorField: "nothing" }, data())).toStrictEqual({}); + }); + + test("should remove `color` if it is not declared in `colorable`", () => { + expect(cleanNavState({ nodeColorField: "n", colorable: ["a-n"] }, data())).toStrictEqual({ colorable: ["a-n"] }); + }); + + test("should remove `size` if it does not exist in the graph", () => { + expect(cleanNavState({ nodeSizeField: "nothing" }, data())).toStrictEqual({}); + }); + + test("should remove `size` if it is not declared in `sizeable`", () => { + expect(cleanNavState({ nodeSizeField: "a-n", sizeable: ["c-n"] }, data())).toStrictEqual({ sizeable: ["c-n"] }); + }); + + test("should remove `size` if it is not a range field", () => { + expect(cleanNavState({ nodeSizeField: "n", sizeable: ["n"] }, data())).toStrictEqual({}); + }); + + test("should remove empty `filters` array", () => { + expect(cleanNavState({ filters: [] }, data())).toStrictEqual({}); + }); + + test("should clean `filters` array", () => { + expect( + cleanNavState({ filters: [{ type: "terms", field: "n", values: ["Doe", "Obama"] }] }, data()), + ).toStrictEqual({ filters: [{ type: "terms", field: "n", values: ["Doe"] }] }); + }); + + test("should remove `nodeSizeRatio` if it is not a proper number", () => { + expect(cleanNavState({ nodeSizeRatio: "5" as any }, data())).toStrictEqual({}); + }); + test("should remove `nodeSizeRatio` if it is the default correction ratio", () => { + expect(cleanNavState({ nodeSizeRatio: DEFAULT_NODE_SIZE_RATIO }, data())).toStrictEqual({}); + }); + test("should clamp `nodeSizeRatio` if it is out of the tolerance range", () => { + expect(cleanNavState({ nodeSizeRatio: 0.001 }, data())).toStrictEqual({ nodeSizeRatio: MIN_NODE_SIZE_RATIO }); + expect(cleanNavState({ nodeSizeRatio: 1000 }, data())).toStrictEqual({ nodeSizeRatio: MAX_NODE_SIZE_RATIO }); + }); + + test("should remove `edgeSizeRatio` if it is not a proper number", () => { + expect(cleanNavState({ edgeSizeRatio: "5" as any }, data())).toStrictEqual({}); + }); + test("should remove `edgeSizeRatio` if it is the default correction ratio", () => { + expect(cleanNavState({ edgeSizeRatio: DEFAULT_NODE_SIZE_RATIO }, data())).toStrictEqual({}); + }); + test("should clamp `edgeSizeRatio` if it is out of the tolerance range", () => { + expect(cleanNavState({ edgeSizeRatio: 0.001 }, data())).toStrictEqual({ edgeSizeRatio: MIN_EDGE_SIZE_RATIO }); + expect(cleanNavState({ edgeSizeRatio: 1000 }, data())).toStrictEqual({ edgeSizeRatio: MAX_EDGE_SIZE_RATIO }); + }); + + test("should remove `edgeColoring` if it is not a proper value", () => { + expect(cleanNavState({ edgeColoring: "woopsy" as any }, data())).toStrictEqual({}); + }); + test("should remove `edgeColoring` if it is the default value", () => { + expect(cleanNavState({ edgeColoring: DEFAULT_EDGE_COLORING }, data())).toStrictEqual({}); + }); + test("should keep `edgeColoring` otherwise", () => { + expect(cleanNavState({ edgeColoring: "o" }, data())).toStrictEqual({ edgeColoring: "o" }); + expect(cleanNavState({ edgeColoring: "s" }, data())).toStrictEqual({ edgeColoring: "s" }); + expect(cleanNavState({ edgeColoring: "t" }, data())).toStrictEqual({ edgeColoring: "t" }); + }); + + test("should remove `edgeDirection` if it is not a proper value", () => { + expect(cleanNavState({ edgeDirection: "woopsy" as any }, data())).toStrictEqual({}); + }); + test("should remove `edgeDirection` if it is the default value", () => { + expect(cleanNavState({ edgeDirection: DEFAULT_EDGE_DIRECTION }, data())).toStrictEqual({}); + }); + test("should keep `edgeDirection` otherwise", () => { + expect(cleanNavState({ edgeDirection: "d" }, data())).toStrictEqual({ edgeDirection: "d" }); + expect(cleanNavState({ edgeDirection: "u" }, data())).toStrictEqual({ edgeDirection: "u" }); + }); + + test("should remove `showGraphMeta` if it is not `true`", () => { + expect(cleanNavState({ showGraphMeta: false }, data())).toStrictEqual({}); + expect(cleanNavState({ showGraphMeta: 123 as any }, data())).toStrictEqual({}); + }); + test("should keep `showGraphMeta` if it is `true`", () => { + expect(cleanNavState({ showGraphMeta: true }, data())).toStrictEqual({ showGraphMeta: true }); + }); + + test("should remove min/max label size if it is not proper numbers", () => { + expect(cleanNavState({ minLabelSize: "5" as any, maxLabelSize: "25" as any }, data())).toStrictEqual({}); + }); + test("should remove min/max label size if they are the default correction ratio", () => { + expect( + cleanNavState({ minLabelSize: DEFAULT_LABEL_SIZE, maxLabelSize: DEFAULT_LABEL_SIZE }, data()), + ).toStrictEqual({}); + }); + test("should clamp min/max label size if out of the tolerance range", () => { + expect(cleanNavState({ minLabelSize: 0.001, maxLabelSize: 0.001 }, data())).toStrictEqual({ + minLabelSize: MIN_LABEL_SIZE, + maxLabelSize: MIN_LABEL_SIZE, + }); + expect(cleanNavState({ minLabelSize: 1000, maxLabelSize: 1000 }, data())).toStrictEqual({ + minLabelSize: MAX_LABEL_SIZE, + maxLabelSize: MAX_LABEL_SIZE, + }); + }); + test("should clamp max label size if lower than min label size", () => { + expect(cleanNavState({ minLabelSize: 7, maxLabelSize: 6 }, data())).toStrictEqual({ + minLabelSize: 7, + maxLabelSize: 7, + }); + }); + + test("should remove `labelThresholdRatio` if it is not a proper number", () => { + expect(cleanNavState({ labelThresholdRatio: "5" as any }, data())).toStrictEqual({}); + }); + test("should remove `labelThresholdRatio` if it is the default correction ratio", () => { + expect(cleanNavState({ labelThresholdRatio: DEFAULT_LABEL_THRESHOLD }, data())).toStrictEqual({}); + }); + test("should clamp `labelThresholdRatio` if it is out of the tolerance range", () => { + expect(cleanNavState({ labelThresholdRatio: 0.001 }, data())).toStrictEqual({ + labelThresholdRatio: MIN_LABEL_THRESHOLD, + }); + expect(cleanNavState({ labelThresholdRatio: 1000 }, data())).toStrictEqual({ + labelThresholdRatio: MAX_LABEL_THRESHOLD, + }); + }); + test("should remove `url` when `local` is true", () => { + expect(cleanNavState({ url: "http://pouet", local: true }, data())).toStrictEqual({ + local: true, + }); + }); + test("should remove `local` when it is not `true`", () => { + expect(cleanNavState({ disableDefaultSize: true }, data())).toStrictEqual({}); + }); + test("should remove `disableDefaultSize` when `sizeable` is empty", () => { + expect(cleanNavState({ disableDefaultSize: true }, data())).toStrictEqual({}); + }); + test("should remove `disableDefaultColor` when `colorable` is empty", () => { + expect(cleanNavState({ disableDefaultColor: true }, data())).toStrictEqual({}); + }); + test("should force a `size` value when `disableDefaultSize` is true", () => { + expect(cleanNavState({ disableDefaultSize: true, sizeable: ["a-n", "c-n"] }, data())).toStrictEqual({ + disableDefaultSize: true, + sizeable: ["a-n", "c-n"], + nodeSizeField: "a-n", + }); + expect( + cleanNavState({ disableDefaultSize: true, sizeable: ["a-n", "c-n"], nodeSizeField: "c-n" }, data()), + ).toStrictEqual({ + disableDefaultSize: true, + sizeable: ["a-n", "c-n"], + nodeSizeField: "c-n", + }); + }); + test("should force a `color` value when `disableDefaultColor` is true", () => { + expect(cleanNavState({ disableDefaultColor: true, colorable: ["n", "a-n"] }, data())).toStrictEqual({ + disableDefaultColor: true, + colorable: ["n", "a-n"], + nodeColorField: "n", + }); + expect( + cleanNavState({ disableDefaultColor: true, colorable: ["n", "a-n"], nodeColorField: "a-n" }, data()), + ).toStrictEqual({ + disableDefaultColor: true, + colorable: ["n", "a-n"], + nodeColorField: "a-n", + }); + }); + }); + + describe("#navStateToQueryURL", () => { + test("should work with base keys", () => { + expect( + navStateToQueryURL({ + url: "foobar", + role: "d", + nodeColorField: "name", + nodeSizeField: "age", + selectedNode: "John", + subtitleFields: ["age"], + }), + ).toBe("url=foobar&r=d&c=name&s=age&n=John&st=age"); + expect( + navStateToQueryURL({ + local: true, + minLabelSize: 10, + maxLabelSize: 15, + nodeSizeRatio: 0.5, + edgeSizeRatio: 0.7, + edgeColoring: "s", + edgeDirection: "d", + showGraphMeta: true, + labelThresholdRatio: 2, + }), + ).toBe("l=1&nr=0.5&er=0.7&ec=s&ed=d&gm=1<=2&ls=10&le=15"); + }); + + test("should work with one filter", () => { + expect( + navStateToQueryURL({ + filters: [{ type: "terms", field: "name", values: ["Doe"] }], + }), + ).toBe("name.t=Doe"); + }); + + test("should work with multiple filters", () => { + expect( + navStateToQueryURL({ + filters: [ + { type: "terms", field: "name", values: ["Doe", "Black"] }, + { type: "range", field: "age", min: 10, max: 30 }, + { type: "search", field: "content", value: "loremipsum", normalizedValue: normalize("loremipsum") }, + ], + }), + ).toBe("name.t[]=Doe&name.t[]=Black&age.min=10&age.max=30&content.v=loremipsum"); + }); + + test("should handle properly filters encoding", () => { + expect( + navStateToQueryURL({ + filters: [{ type: "terms", field: "name&firstname", values: ['Jean, "Jacques"', "José"] }], + }), + ).toBe("name%26firstname.t[]=Jean%2C%20%22Jacques%22&name%26firstname.t[]=Jos%C3%A9"); + }); + + test("should work with filterable fields", () => { + expect(navStateToQueryURL({ filterable: ["age", "name"] })).toBe("fa[]=age&fa[]=name"); + }); + }); + + describe("#queryURLToNavState", () => { + test("should work with base keys", () => { + expect(queryURLToNavState("url=foobar&r=d&c=name&s=age&n=John&st[]=age")).toStrictEqual({ + url: "foobar", + role: "d", + nodeColorField: "name", + nodeSizeField: "age", + selectedNode: "John", + subtitleFields: ["age"], + }); + expect(queryURLToNavState("l=1&nr=0.5&er=0.7&ec=s&ed=d&gm=1<=2&ls=10&le=15")).toStrictEqual({ + local: true, + minLabelSize: 10, + maxLabelSize: 15, + nodeSizeRatio: 0.5, + edgeSizeRatio: 0.7, + edgeColoring: "s", + edgeDirection: "d", + showGraphMeta: true, + labelThresholdRatio: 2, + }); + }); + + test("should work with one filter", () => { + expect(queryURLToNavState("name.t=Doe")).toStrictEqual({ + filters: [{ type: "terms", field: "name", values: ["Doe"] }], + }); + }); + + test("should work with filters", () => { + expect( + queryURLToNavState("name.t[]=Doe&name.t[]=Black&age.min=10&age.max=30&content.v=loremipsum"), + ).toStrictEqual({ + filters: [ + { type: "terms", field: "name", values: ["Doe", "Black"] }, + { type: "range", field: "age", min: 10, max: 30 }, + { type: "search", field: "content", value: "loremipsum", normalizedValue: normalize("loremipsum") }, + ], + }); + }); + + test("should handle properly filters encoding", () => { + expect( + queryURLToNavState("name%26firstname.t[]=Jean%2C%20%22Jacques%22&name%26firstname.t[]=Jos%C3%A9"), + ).toStrictEqual({ + filters: [{ type: "terms", field: "name&firstname", values: ['Jean, "Jacques"', "José"] }], + }); + }); + + test("should work with filterable fields", () => { + expect(queryURLToNavState("fa=age")).toStrictEqual({ filterable: ["age"] }); + expect(queryURLToNavState("fa[]=age")).toStrictEqual({ filterable: ["age"] }); + expect(queryURLToNavState("fa=age&fa=name")).toStrictEqual({ filterable: ["age", "name"] }); + expect(queryURLToNavState("fa[]=age&fa[]=name")).toStrictEqual({ filterable: ["age", "name"] }); + }); + }); +}); diff --git a/src/lib/navState.ts b/src/lib/navState.ts new file mode 100644 index 0000000..030cb3d --- /dev/null +++ b/src/lib/navState.ts @@ -0,0 +1,338 @@ +import { clamp, groupBy, isNil, keyBy, map, omitBy, uniq } from "lodash"; + +import { arrayify } from "../utils/array"; +import { normalize } from "../utils/string"; +import { queryStringToRecord, urlSearchParamsToString } from "../utils/url"; +import { + DEFAULT_EDGE_SIZE_RATIO, + DEFAULT_LABEL_SIZE, + DEFAULT_LABEL_THRESHOLD, + DEFAULT_NODE_SIZE_RATIO, + MAX_EDGE_SIZE_RATIO, + MAX_LABEL_SIZE, + MAX_LABEL_THRESHOLD, + MAX_NODE_SIZE_RATIO, + MIN_EDGE_SIZE_RATIO, + MIN_LABEL_SIZE, + MIN_LABEL_THRESHOLD, + MIN_NODE_SIZE_RATIO, +} from "./consts"; +import { Data, FieldType, QualiField, Report } from "./data"; + +export interface SearchFilter { + type: "search"; + field: string; + value: string; + normalizedValue: string; +} +export interface TermsFilter { + type: "terms"; + field: string; + values: string[]; +} +export interface RangeFilter { + type: "range"; + field: string; + min?: number; + max?: number; +} +export type Filter = SearchFilter | TermsFilter | RangeFilter; +export type FilterType = Filter["type"]; + +export const FILTER_FIELD_TYPES: Record = { + search: "content", + range: "quanti", + terms: "quali", +}; + +// Stand for "edit", "explore" or "view" +export const ROLES = ["d", "x", "v"] as const; +export const ROLES_SET: Set = new Set(ROLES); +export type Role = (typeof ROLES)[number]; +export const DEFAULT_ROLE: Role = "d"; + +// Stand for "source", "target", "original" or "constant" +export const EDGE_COLORING_MODES = ["c", "o", "s", "t"] as const; +export const EDGE_COLORING_MODES_SET: Set = new Set(EDGE_COLORING_MODES); +export type EdgeColoring = (typeof EDGE_COLORING_MODES)[number]; +export const DEFAULT_EDGE_COLORING: EdgeColoring = "c"; + +// Stand for "original", "directed" or "undirected" +export const EDGE_DIRECTION_MODES = ["o", "d", "u"] as const; +export const EDGE_DIRECTION_MODES_SET: Set = new Set(EDGE_DIRECTION_MODES); +export type EdgeDirection = (typeof EDGE_DIRECTION_MODES)[number]; +export const DEFAULT_EDGE_DIRECTION: EdgeDirection = "o"; + +export interface NavState { + url?: string | undefined; + local?: boolean | undefined; + role?: Role | undefined; + + // Editor state: + sizeable?: string[] | undefined; + colorable?: string[] | undefined; + filterable?: string[] | undefined; + subtitleFields?: string[] | undefined; + disableDefaultSize?: boolean | undefined; + disableDefaultColor?: boolean | undefined; + showGraphMeta?: boolean | undefined; + + // Viewer state: + nodeSizeField?: string | undefined; + nodeColorField?: string | undefined; + filters?: Filter[] | undefined; + selectedNode?: string | undefined; + nodeSizeRatio?: number | undefined; + edgeSizeRatio?: number | undefined; + useEdgeWeights?: boolean | undefined; + labelThresholdRatio?: number | undefined; + minLabelSize?: number | undefined; + maxLabelSize?: number | undefined; + edgeColoring?: EdgeColoring | undefined; + edgeDirection?: EdgeDirection | undefined; + + // Only for some specific transitions: + preventBlocker?: boolean; +} + +export function cleanFilter(filter: Filter, data: Data): Filter | null { + const field = data.fieldsIndex[filter.field]; + if (!field || field.type !== FILTER_FIELD_TYPES[filter.type]) return null; + + if (filter.type === "terms") { + const valuesIndex = (field as QualiField).values; + const values = filter.values.filter((v) => valuesIndex[v]); + return values.length ? { ...filter, values } : null; + } else if (filter.type === "range") { + return typeof filter.min === "number" || typeof filter.max === "number" ? filter : null; + } else { + return filter.value ? filter : null; + } +} + +export function cleanNavState(state: NavState, data: Data): NavState { + const { graph, fieldsIndex } = data; + const { + sizeable, + colorable, + filterable, + subtitleFields, + nodeColorField, + nodeSizeField, + filters, + selectedNode, + nodeSizeRatio, + edgeSizeRatio, + edgeColoring, + edgeDirection, + minLabelSize, + maxLabelSize, + showGraphMeta, + labelThresholdRatio, + disableDefaultSize, + disableDefaultColor, + } = state; + + const cleanedSubtitleFields = uniq((subtitleFields || []).filter((f) => fieldsIndex[f])); + const cleanedColorable = uniq((colorable || []).filter((f) => fieldsIndex[f])); + const cleanedSizeable = uniq((sizeable || []).filter((f) => fieldsIndex[f]?.type === "quanti")); + const cleanedFilterable = uniq( + (filterable || []).filter((f) => fieldsIndex[f] && !cleanedSizeable.includes(f) && !cleanedColorable.includes(f)), + ); + const cleanedSizeableIndex = keyBy(cleanedSizeable); + const cleanedColorableIndex = keyBy(cleanedColorable); + + const cleanedDisableDefaultSize = disableDefaultSize && !!cleanedSizeable.length; + const cleanedDisableDefaultColor = disableDefaultColor && !!cleanedColorable.length; + + const cleanedFilters = (filters || []) + .map((filter) => cleanFilter(filter, data)) + .filter((filter) => !isNil(filter)) as Filter[]; + + const cleanedMinLabelSize = clamp( + typeof minLabelSize === "number" ? minLabelSize : DEFAULT_LABEL_SIZE, + MIN_LABEL_SIZE, + MAX_LABEL_SIZE, + ); + const cleanedMaxLabelSize = clamp( + typeof maxLabelSize === "number" ? maxLabelSize : DEFAULT_LABEL_SIZE, + cleanedMinLabelSize, + MAX_LABEL_SIZE, + ); + + const cleanedState: NavState = { + local: state.local, + url: !state.local ? state.url : undefined, + role: state.role && ROLES_SET.has(state.role) && state.role !== DEFAULT_ROLE ? state.role : undefined, + // Editor state: + sizeable: cleanedSizeable.length ? cleanedSizeable : undefined, + colorable: cleanedColorable.length ? cleanedColorable : undefined, + filterable: cleanedFilterable.length ? cleanedFilterable : undefined, + subtitleFields: cleanedSubtitleFields.length ? cleanedSubtitleFields : undefined, + // Viewer state: + nodeSizeField: + (nodeSizeField && fieldsIndex[nodeSizeField] && cleanedSizeableIndex[nodeSizeField] + ? nodeSizeField + : undefined) || (cleanedDisableDefaultSize ? cleanedSizeable[0] : undefined), + nodeColorField: + (nodeColorField && fieldsIndex[nodeColorField] && cleanedColorableIndex[nodeColorField] + ? nodeColorField + : undefined) || (cleanedDisableDefaultColor ? cleanedColorable[0] : undefined), + filters: cleanedFilters.length ? cleanedFilters : undefined, + selectedNode: graph.hasNode(selectedNode) ? selectedNode : undefined, + nodeSizeRatio: + typeof nodeSizeRatio === "number" && nodeSizeRatio !== DEFAULT_NODE_SIZE_RATIO + ? clamp(nodeSizeRatio, MIN_NODE_SIZE_RATIO, MAX_NODE_SIZE_RATIO) + : undefined, + edgeSizeRatio: + typeof edgeSizeRatio === "number" && edgeSizeRatio !== DEFAULT_EDGE_SIZE_RATIO + ? clamp(edgeSizeRatio, MIN_EDGE_SIZE_RATIO, MAX_EDGE_SIZE_RATIO) + : undefined, + labelThresholdRatio: + typeof labelThresholdRatio === "number" && labelThresholdRatio !== DEFAULT_LABEL_THRESHOLD + ? clamp(labelThresholdRatio, MIN_LABEL_THRESHOLD, MAX_LABEL_THRESHOLD) + : undefined, + edgeColoring: + edgeColoring && EDGE_COLORING_MODES_SET.has(edgeColoring) && edgeColoring !== DEFAULT_EDGE_COLORING + ? edgeColoring + : undefined, + edgeDirection: + edgeDirection && EDGE_DIRECTION_MODES_SET.has(edgeDirection) && edgeDirection !== DEFAULT_EDGE_DIRECTION + ? edgeDirection + : undefined, + showGraphMeta: showGraphMeta === true ? true : undefined, + minLabelSize: cleanedMinLabelSize !== DEFAULT_LABEL_SIZE ? cleanedMinLabelSize : undefined, + maxLabelSize: cleanedMaxLabelSize !== DEFAULT_LABEL_SIZE ? cleanedMaxLabelSize : undefined, + disableDefaultSize: cleanedDisableDefaultSize || undefined, + disableDefaultColor: cleanedDisableDefaultColor || undefined, + }; + + return omitBy(cleanedState, isNil) as NavState; +} + +export function navStateToQueryURL(state: NavState): string { + const params = new URLSearchParams(); + + if (state.url) params.append("url", state.url); + if (state.local) params.append("l", "1"); + if (state.role) params.append("r", state.role); + if (state.nodeColorField) params.append("c", state.nodeColorField); + if (state.nodeSizeField) params.append("s", state.nodeSizeField); + if (state.selectedNode) params.append("n", state.selectedNode); + if (state.sizeable) state.sizeable.forEach((f) => params.append("sa", f)); + if (state.colorable) state.colorable.forEach((f) => params.append("ca", f)); + if (state.filterable) state.filterable.forEach((f) => params.append("fa", f)); + if (state.subtitleFields) state.subtitleFields.forEach((f) => params.append("st", f)); + if (state.filters) { + state.filters.forEach((filter) => { + if (filter.type === "terms") { + const key = `${filter.field}.t`; + filter.values.forEach((s) => params.append(key, s)); + } else if (filter.type === "range") { + if (typeof filter.min === "number") params.append(`${filter.field}.min`, filter.min + ""); + if (typeof filter.max === "number") params.append(`${filter.field}.max`, filter.max + ""); + } else { + params.append(`${filter.field}.v`, filter.value + ""); + } + }); + } + if (state.nodeSizeRatio) params.append("nr", state.nodeSizeRatio + ""); + if (state.edgeSizeRatio) params.append("er", state.edgeSizeRatio + ""); + if (state.edgeColoring) params.append("ec", state.edgeColoring); + if (state.edgeDirection) params.append("ed", state.edgeDirection); + if (state.showGraphMeta) params.append("gm", "1"); + if (state.labelThresholdRatio) params.append("lt", state.labelThresholdRatio + ""); + if (state.disableDefaultSize) params.append("ds", "1"); + if (state.disableDefaultColor) params.append("dc", "1"); + if (state.minLabelSize) params.append("ls", state.minLabelSize + ""); + if (state.maxLabelSize) params.append("le", state.maxLabelSize + ""); + + return urlSearchParamsToString(params); +} + +export function queryURLToNavState(queryURL: string): NavState { + const { url, l, r, s, c, n, fa, ca, sa, le, ls, nr, er, ec, ed, gm, lt, ds, dc, st, ...query } = + queryStringToRecord(queryURL); + const navState: NavState = {}; + + if (typeof url === "string") navState.url = url; + if (typeof l === "string") navState.local = l === "1"; + if (typeof r === "string" && ROLES_SET.has(r)) navState.role = r as Role; + if (typeof s === "string") navState.nodeSizeField = s; + if (typeof c === "string") navState.nodeColorField = c; + if (typeof n === "string") navState.selectedNode = n; + if (typeof nr === "string") navState.nodeSizeRatio = +nr; + if (typeof er === "string") navState.edgeSizeRatio = +er; + if (typeof ec === "string") navState.edgeColoring = ec as EdgeColoring; + if (typeof ed === "string") navState.edgeDirection = ed as EdgeDirection; + if (typeof gm === "string") navState.showGraphMeta = gm === "1"; + if (typeof ls === "string") navState.minLabelSize = +ls; + if (typeof le === "string") navState.maxLabelSize = +le; + if (typeof lt === "string") navState.labelThresholdRatio = +lt; + if (typeof ds === "string") navState.disableDefaultSize = ds === "1"; + if (typeof dc === "string") navState.disableDefaultColor = dc === "1"; + if (sa) navState.sizeable = arrayify(sa); + if (ca) navState.colorable = arrayify(ca); + if (fa) navState.filterable = arrayify(fa); + if (st) navState.subtitleFields = arrayify(st); + + const fields = groupBy(Object.keys(query), (key) => key.replace(/\.(v|t|min|max)$/, "")); + const filters = map(fields, ([q0, q1], field): Filter => { + // Terms case: + if (q0 === `${field}.t`) { + return { + type: "terms", + field, + values: arrayify(query[q0]), + }; + // Search case: + } else if (q0 === `${field}.v`) { + return { + type: "search", + field, + value: query[q0] + "", + normalizedValue: normalize(query[q0] + ""), + }; + // Range case: + } else { + const filter: Filter = { + type: "range", + field, + }; + [q0, q1].forEach((key) => { + if (key === `${field}.min`) filter.min = +query[key]; + if (key === `${field}.max`) filter.max = +query[key]; + }); + return filter; + } + }); + if (filters.length) navState.filters = filters; + + return navState; +} + +export function guessNavState(data: Data, report: Report): NavState { + const { fields, fieldsIndex, graph } = data; + + const colorable = fields.filter( + (f) => fieldsIndex[f].type === "quali" && fieldsIndex[f].nullValuesCount <= graph.order * 0.5, + ); + const colorableRawFieldIDsSet = new Set(colorable.map((f) => fieldsIndex[f].rawFieldId)); + const sizeable = fields.filter( + (f) => + fieldsIndex[f].type === "quanti" && + !colorableRawFieldIDsSet.has(fieldsIndex[f].rawFieldId) && + fieldsIndex[f].nullValuesCount <= graph.order * 0.5, + ); + + return cleanNavState( + { + sizeable, + colorable, + nodeSizeField: (report.missingNodeSizes || 0) >= graph.order * 0.5 ? sizeable[0] : undefined, + nodeColorField: (report.missingNodeColors || 0) >= graph.order * 0.5 ? colorable[0] : undefined, + edgeColoring: (report.missingEdgeColors || 0) >= graph.size * 0.5 ? "c" : "o", + }, + data, + ); +} diff --git a/src/lib/notifications.ts b/src/lib/notifications.ts new file mode 100644 index 0000000..58d7660 --- /dev/null +++ b/src/lib/notifications.ts @@ -0,0 +1,47 @@ +import { atom, useAtom } from "jotai"; +import { useCallback } from "react"; + +export interface NotificationInput { + message: string | JSX.Element; + type?: "success" | "info" | "warning" | "error"; + + // Options: + keepAlive?: boolean; +} + +interface NotificationTechnical extends NotificationInput { + id: string; + createdAt: number; +} + +const notificationsAtom = atom([]); + +let INCREMENTAL_ID = 1; + +export function useNotifications() { + const [notifications, setNotifications] = useAtom(notificationsAtom); + + const notify = useCallback( + (notification: NotificationInput) => { + const id = ++INCREMENTAL_ID + ""; + const fullNotification = { + ...notification, + createdAt: Date.now(), + id, + }; + setNotifications((notifications) => notifications.concat([fullNotification])); + + return id; + }, + [setNotifications], + ); + + const remove = useCallback( + (id: string) => { + setNotifications((notifications) => notifications.filter((notification) => notification.id !== id)); + }, + [setNotifications], + ); + + return { notifications, notify, remove }; +} diff --git a/src/styles/_animations.scss b/src/styles/_animations.scss new file mode 100644 index 0000000..76023db --- /dev/null +++ b/src/styles/_animations.scss @@ -0,0 +1,22 @@ +.fade-enter { + opacity: 0; + transform: scale(0.9); +} +.fade-enter-active { + opacity: 1; + transform: translateX(0); + transition: + opacity 300ms, + transform 300ms; +} + +.fade-exit { + opacity: 1; +} +.fade-exit-active { + opacity: 0; + transform: scale(0.9); + transition: + opacity 300ms, + transform 300ms; +} diff --git a/src/styles/_base.scss b/src/styles/_base.scss new file mode 100644 index 0000000..c5e478c --- /dev/null +++ b/src/styles/_base.scss @@ -0,0 +1,183 @@ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: $font-family-serif; +} + +hr { + margin: 2em 0; +} + +.text-ellipsis { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.circle, +.disc { + display: inline-block; + height: 1em; + width: 1em; + border-radius: 1em; + vertical-align: top; +} +.circle { + position: relative; + + &::after { + content: " "; + position: absolute; + inset: 1px; + border-radius: 1em; + background: white; + } +} +.hoverable:hover { + cursor: pointer; + opacity: 0.7; +} + +.triangle { + width: 0; + height: 0; + border-top: 0.3em solid transparent; + border-bottom: 0.3em solid transparent; +} +.triangle-left { + border-right: 0.8em solid $arrow-color; +} +.triangle-right { + border-left: 0.8em solid $arrow-color; +} +.line { + width: 2.6em; + height: 0.25em; + background: $arrow-color; + position: relative; + + &:first-child:last-child { + width: 3.4em; + } + + .count { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + + padding: 0.3em; + background: white; + + box-sizing: content-box; + } +} + +.edge, +.edges { + display: inline-block; + width: 3em; + text-align: center; +} + +.btn > svg { + vertical-align: text-bottom; +} +.btn-inline { + line-height: 1em; +} + +.w-1 { + width: 1% !important; +} +.w-45 { + width: 45% !important; +} +.line-height-1 { + line-height: 1em !important; +} + +.flex-regular-width { + flex-shrink: 1 !important; + flex-grow: 1 !important; + flex-basis: 0 !important; + width: 0 !important; +} + +input:placeholder-shown { + text-overflow: ellipsis; + overflow: hidden; +} + +.cursor-pointer { + cursor: pointer !important; +} + +.input-inline { + width: 5em; +} + +.custom-scrollbar { + scrollbar-color: grey whitesmoke; + scrollbar-width: thin; + + &::-webkit-scrollbar-track { + background-color: whitesmoke; + } + + &::-webkit-scrollbar { + width: 3px; + height: 3px; + background-color: #fff; + } + + &::-webkit-scrollbar-thumb { + background-color: grey; + } +} + +input[type="number"] { + -moz-appearance: textfield; +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; +} + +/** + * Vendors: + */ +.react-select__menu-portal { + z-index: $zindex-tooltip !important; +} +.react-select__control { + border-color: black !important; +} +.react-select__menu { + margin-bottom: 1em; +} +.react-select__menu-list { + @extend .custom-scrollbar; +} +.react-select__control--is-focused { + box-shadow: 0 0 0 1px black !important; +} + +.scrollbar-left { + direction: rtl; + + & > * { + direction: ltr; + } +} + +.dropzone { + padding: 4rem; + border-radius: 1rem; + border: 3px dotted black; + width: 100%; +} diff --git a/src/styles/_filters.scss b/src/styles/_filters.scss new file mode 100644 index 0000000..c336aa0 --- /dev/null +++ b/src/styles/_filters.scss @@ -0,0 +1,105 @@ +$viz-in-color: #333; +$viz-out-color: #999; + +.terms-filter { + $bar-height: 5px; + + .term { + margin-bottom: 0.1em; + border-radius: 5px; + padding: 2px; + + &.editable .value { + cursor: pointer; + } + .value:hover { + opacity: 0.7; + } + + .value span { + color: $viz-out-color; + } + + &.active .value span { + color: $viz-in-color; + } + } + + .bar { + height: $bar-height; + position: relative; + + .global, + .filtered { + position: absolute; + left: 0; + top: 0; + bottom: 0; + transition: width ease-in-out 0.2s; + } + + .global { + background: $gray-300; + } + .filtered { + background: $gray-800; + } + } +} + +.range-filter { + height: 160px; + + display: flex; + flex-direction: row; + justify-content: space-between; + + .bar { + position: relative; + height: 100%; + flex-grow: 1; + + &:not(:last-child) { + margin-right: 1px; + } + } + + .global, + .filtered { + position: absolute; + left: 0; + right: 0; + bottom: 0; + transition: height ease-in-out 0.2s; + } + + .global { + background: $gray-300; + } + .filtered { + background: $gray-800; + } + .label { + @extend .ellipsis; + + position: absolute; + text-align: center; + width: 100%; + font-size: 0.8em; + + &.inside { + top: 0; + } + &.outside { + bottom: 100%; + color: $text-muted; + } + } +} + +.rc-slider-mark-text { + color: $viz-out-color !important; +} +.rc-slider-mark-text-active { + color: $viz-in-color !important; +} diff --git a/src/styles/_graph.scss b/src/styles/_graph.scss new file mode 100644 index 0000000..50b4721 --- /dev/null +++ b/src/styles/_graph.scss @@ -0,0 +1,117 @@ +.graph-view { + $stage-margin: 1rem; + $button-size: 2em; + + .graph-button { + width: $button-size; + height: $button-size; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5em; + margin-bottom: 0.4em; + background: white; + + &:hover { + background: black; + } + } + + .graph { + position: relative; + flex-grow: 1; + + .controls { + position: absolute; + top: $stage-margin; + right: $stage-margin; + display: flex; + flex-direction: column; + align-items: flex-end; + + & > * { + z-index: $zindex-buttons; + } + } + + .captions { + position: absolute; + bottom: $stage-margin; + left: $stage-margin; + + .size-caption { + z-index: $zindex-caption; + + .nodes { + display: flex; + flex-direction: row; + align-items: flex-end; + } + + .circle-wrapper { + height: 50px; + overflow: hidden; + display: flex; + align-items: center; + min-width: 30px; + justify-content: center; + } + + .dotted-circle { + border-radius: 100%; + background: #cccccc66; + border: 2px dotted black; + } + } + } + + .sigma-wrapper { + width: 100%; + height: 100%; + overflow: hidden; + position: relative; + } + + .sigma-container { + position: absolute; + width: 100vw; + height: 100%; + + left: 50%; + margin-left: -50vw; + + background: #fcfcfc; + + .sigma-mouse { + z-index: $zindex-sigma-mouse; + } + } + + // Sigma layer is behind everything else, despite being absolutely placed: + & > * { + z-index: 1; + } + .sigma { + z-index: 0; + } + } + + .context-panel { + background: white; + } + + .toggle-button { + position: absolute; + top: $stage-margin; + left: $stage-margin; + z-index: $zindex-buttons; + } +} + +// Inside a portal, but spawned from the GraphControl component: +.search-node.active-node, +.search-node:hover { + background: #eee; + cursor: pointer; +} diff --git a/src/styles/_home.scss b/src/styles/_home.scss new file mode 100644 index 0000000..16dcc68 --- /dev/null +++ b/src/styles/_home.scss @@ -0,0 +1,32 @@ +.home-view { + display: flex; + flex-direction: column; + min-height: 100vh; + + .expanding-block { + width: 100%; + } + + .title-block { + flex-grow: 1; + + padding: 20vh 0.5rem 0.5rem; + max-width: 500px; + margin: 0 auto; + text-align: center; + + min-height: 85vh; + } + + .gexf-form { + transition: opacity ease-in-out 0.6s; + } + + .footer { + flex-shrink: 0; + + width: 600px; + max-width: 100%; + margin: 0 auto; + } +} diff --git a/src/styles/_layout.scss b/src/styles/_layout.scss new file mode 100644 index 0000000..e2eda0b --- /dev/null +++ b/src/styles/_layout.scss @@ -0,0 +1,151 @@ +body { + padding: 0; + margin: 0; + + #root { + display: flex; + flex-direction: row; + background: white; + align-items: stretch; + overflow: hidden; + + width: 100vw; + height: 100vh; + + main { + flex-grow: 1; + } + } + + #toasts-container { + position: fixed; + bottom: 0; + right: 0; + z-index: $zindex-tooltip; + + .toast { + max-width: calc(100vw - 1rem); + } + } + + #portal-target { + position: absolute; + top: 0; + left: 0; + } +} + +// Side panels layout: +.side-panel { + border-right: 1px solid $border-color; + overflow: hidden; + height: 100%; + + z-index: $zindex-panel; + + display: flex; + flex-direction: column; + + .block { + &:not(:last-child) { + border-bottom: 1px solid $border-color; + } + } + + .panel-header { + border-bottom: 1px solid $border-color; + flex-shrink: 0; + + .header-buttons { + padding-left: 4.5em !important; + } + } + + .panel-content { + flex-shrink: 1; + flex-grow: 1; + flex-basis: 0; + + display: flex; + flex-direction: column; + + @extend .custom-scrollbar; + overflow-y: scroll; + + & > * > *:not(hr) { + padding: 1rem; + } + } +} + +// Graph container layout: +.graph-view { + height: 100%; + position: relative; + overflow: hidden; + + .wrapper { + position: absolute; + inset: 0 0 0 auto; + + display: flex; + flex-direction: row; + + transition: width $base-transition; + } +} +.edition-panel { + transition: all $base-transition; + z-index: $zindex-panel + 1; + + &.expanded { + box-shadow: 5px 0 15px rgba($black, 0.35); + } +} + +@include media-breakpoint-up(md) { + $panelSize: 500px; + + .graph-view.panel-collapsed .wrapper { + width: calc(#{$panelSize} + 100%); + } + .graph-view.panel-expanded .wrapper { + width: 100%; + } + .side-panel { + width: $panelSize; + } + .edition-panel.collapsed { + margin-left: -$panelSize; + } +} +@include media-breakpoint-down(md) { + .graph-view.panel-collapsed .wrapper { + width: 200%; + } + .graph-view.panel-expanded .wrapper { + width: 100%; + } + .side-panel { + width: 100vw; + } + .edition-panel.collapsed { + margin-left: -100vw; + } +} + +// Modals layout: +.modal { + display: block; + + .modal-backdrop { + opacity: 0.1; + z-index: $zindex-modal-backdrop; + } + .modal-content { + z-index: $zindex-modal; + } + .modal-body { + @extend .custom-scrollbar; + } +} diff --git a/src/styles/_utils.scss b/src/styles/_utils.scss new file mode 100644 index 0000000..5ce1354 --- /dev/null +++ b/src/styles/_utils.scss @@ -0,0 +1,44 @@ +.flex-centered { + display: flex; + align-items: center; + justify-content: center; +} + +.fill { + position: absolute; + inset: 0; +} + +.hidden { + visibility: hidden; +} + +.with-end-buttons { + display: flex; + flex-direction: row; + align-items: center; + + & > *:first-child { + flex-grow: 1; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + line-height: 1.8em; + } + + & > *:not(:first-child) { + flex-shrink: 0; + } +} + +.ellipsis { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.box { + display: inline-block; + width: 1.5em; + text-align: center; +} diff --git a/src/styles/_variables-override.scss b/src/styles/_variables-override.scss new file mode 100644 index 0000000..056d821 --- /dev/null +++ b/src/styles/_variables-override.scss @@ -0,0 +1,16 @@ +// Bootstrap variables can be overridden here: +$green: #7bd16c !default; +$cyan: #c9dbed !default; +$gray-700: #495057 !default; + +$font-size-base: 0.875rem !default; +$font-family-sans-serif: "Public Sans", Helvetica, Arial, sans-serif !default; +$font-family-serif: Sanchez, sans-serif !default; + +$link-color: $gray-700 !default; +$btn-link-color: $link-color; +$link-hover-color: black !default; +$btn-border-radius: 0.25rem !default; +$table-bg: transparent !default; + +$toast-max-width: 380px !default; diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss new file mode 100644 index 0000000..b4f44f4 --- /dev/null +++ b/src/styles/_variables.scss @@ -0,0 +1,8 @@ +// Custom variables: +$zindex-caption: 100; +$zindex-sigma-mouse: 101; +$zindex-panel: 102; +$zindex-buttons: 103; +$arrow-color: #ccc; + +$base-transition: ease-in-out 0.5s; diff --git a/src/styles/index.scss b/src/styles/index.scss new file mode 100644 index 0000000..2e6eafd --- /dev/null +++ b/src/styles/index.scss @@ -0,0 +1,60 @@ +// Fonts: +@import url("https://fonts.googleapis.com/css2?family=Sanchez&family=Public+Sans&display=swap"); + +@import "variables-override"; + +// Bootstrap files: +@import "bootstrap/scss/functions"; +@import "bootstrap/scss/variables"; +@import "bootstrap/scss/variables-dark"; + +@import "variables"; + +@import "bootstrap/scss/maps"; +@import "bootstrap/scss/mixins"; +@import "bootstrap/scss/utilities"; +@import "bootstrap/scss/root"; +@import "bootstrap/scss/reboot"; +@import "bootstrap/scss/type"; +@import "bootstrap/scss/images"; +@import "bootstrap/scss/containers"; +@import "bootstrap/scss/grid"; +@import "bootstrap/scss/tables"; +@import "bootstrap/scss/forms"; +@import "bootstrap/scss/buttons"; +@import "bootstrap/scss/transitions"; +@import "bootstrap/scss/dropdown"; +@import "bootstrap/scss/button-group"; +@import "bootstrap/scss/nav"; +@import "bootstrap/scss/navbar"; +@import "bootstrap/scss/card"; +@import "bootstrap/scss/accordion"; +@import "bootstrap/scss/breadcrumb"; +@import "bootstrap/scss/pagination"; +@import "bootstrap/scss/badge"; +@import "bootstrap/scss/alert"; +@import "bootstrap/scss/progress"; +@import "bootstrap/scss/list-group"; +@import "bootstrap/scss/close"; +@import "bootstrap/scss/toasts"; +@import "bootstrap/scss/modal"; +@import "bootstrap/scss/tooltip"; +@import "bootstrap/scss/popover"; +@import "bootstrap/scss/carousel"; +@import "bootstrap/scss/spinners"; +@import "bootstrap/scss/offcanvas"; +@import "bootstrap/scss/placeholders"; +@import "bootstrap/scss/helpers"; +@import "bootstrap/scss/utilities/api"; + +// Other external libraries: +@import "rc-slider/assets/index.css"; + +// Internal files: +@import "base"; +@import "utils"; +@import "layout"; +@import "home"; +@import "graph"; +@import "filters"; +@import "animations"; \ No newline at end of file diff --git a/src/types/glsl.d.ts b/src/types/glsl.d.ts new file mode 100644 index 0000000..5c41b16 --- /dev/null +++ b/src/types/glsl.d.ts @@ -0,0 +1 @@ +declare module "*.glsl"; diff --git a/src/types/iwanthue.d.ts b/src/types/iwanthue.d.ts new file mode 100644 index 0000000..955a388 --- /dev/null +++ b/src/types/iwanthue.d.ts @@ -0,0 +1 @@ +declare module "iwanthue/precomputed/*"; diff --git a/src/utils/array.ts b/src/utils/array.ts new file mode 100644 index 0000000..f0527a2 --- /dev/null +++ b/src/utils/array.ts @@ -0,0 +1,3 @@ +export function arrayify(value: T | T[]): T[] { + return Array.isArray(value) ? value : [value]; +} diff --git a/src/utils/canvas.ts b/src/utils/canvas.ts new file mode 100644 index 0000000..e697a46 --- /dev/null +++ b/src/utils/canvas.ts @@ -0,0 +1,113 @@ +import { Settings } from "sigma/settings"; +import { NodeDisplayData, PartialButFor, PlainObject } from "sigma/types"; + +import { RETINA_HIDDEN_FIELD_PREFIX } from "../lib/consts"; + +/** + * This function draw in the input canvas 2D context a rectangle. + * It only deals with tracing the path, and does not fill or stroke. + */ +export function drawRoundRect( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height: number, + radius: number, +): void { + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); +} + +/** + * Custom hover renderer + */ +export function drawHover(context: CanvasRenderingContext2D, data: PlainObject, settings: PlainObject) { + const size = data.labelSize; + const font = settings.labelFont; + const weight = settings.labelWeight; + const isItalic = data[RETINA_HIDDEN_FIELD_PREFIX + "italic"]; + const subtitleSize = size - 2; + + const label = data.label; + const subtitles: string[] = data.subtitles || []; + const color = settings.labelColor.attribute + ? data[settings.labelColor.attribute] || settings.labelColor.color || "#000" + : settings.labelColor.color; + + // Then we draw the label background + context.beginPath(); + context.fillStyle = "#fff"; + context.shadowOffsetX = 0; + context.shadowOffsetY = 2; + context.shadowBlur = 8; + context.shadowColor = "#000"; + + context.font = `${weight} ${size}px ${font}`; + const labelWidth = context.measureText(label).width; + context.font = `${weight} ${subtitleSize}px ${font}`; + const subtitleWidth = Math.max(0, ...subtitles.map((s: string) => context.measureText(s).width)); + + const textWidth = Math.max(labelWidth, subtitleWidth); + + const x = Math.round(data.x); + const y = Math.round(data.y); + const w = Math.round(textWidth + size / 2 + data.size + 3); + const hLabel = Math.round(size * 0.7); + const hSubtitle = Math.round(subtitleSize * 1.3); + + drawRoundRect(context, x, y - 0.8 * size, w, hSubtitle * subtitles.length + hLabel + 0.8 * size, 5); + context.closePath(); + context.fill(); + + context.shadowOffsetX = 0; + context.shadowOffsetY = 0; + context.shadowBlur = 0; + + // And finally we draw the labels + context.fillStyle = color; + context.font = `${isItalic ? "italic " : ""}${weight} ${size}px ${font}`; + context.fillText(label, data.x + data.size + 3, data.y + size / 3); + + subtitles.forEach((s, i) => { + context.fillStyle = "#666"; + context.font = `${weight} ${subtitleSize}px ${font}`; + context.fillText(s, data.x + data.size + 3, data.y + size / 3 + hSubtitle * (i + 1)); + }); +} + +/** + * Custom label renderer + */ +export default function drawLabel( + context: CanvasRenderingContext2D, + data: PartialButFor, + settings: Settings, +): void { + const label = data.hideLabel ? null : data.label; + + if (!label) return; + + const size = data.labelSize, + font = settings.labelFont, + weight = settings.labelWeight, + isItalic = data[RETINA_HIDDEN_FIELD_PREFIX + "italic"]; + + context.font = `${isItalic ? "italic " : ""}${weight} ${size}px ${font}`; + const width = context.measureText(label).width + 8; + + context.fillStyle = "#ffffff66"; + context.fillRect(data.x + data.size, data.y - (size / 3) * 2, width, size * 1.5); + + context.fillStyle = "#000"; + context.fillText(label, data.x + data.size + 3, data.y + size / 3); +} diff --git a/src/utils/color.ts b/src/utils/color.ts new file mode 100644 index 0000000..bfbb98b --- /dev/null +++ b/src/utils/color.ts @@ -0,0 +1,8 @@ +import chroma from "chroma-js"; + +/** + * This helper determines whether a text should be black or white considering its background color. + */ +export function getFontColor(color: string): "black" | "white" { + return chroma(color).luminance() > 0.5 ? "black" : "white"; +} diff --git a/src/utils/file.ts b/src/utils/file.ts new file mode 100644 index 0000000..0705cbe --- /dev/null +++ b/src/utils/file.ts @@ -0,0 +1,15 @@ +export function saveFileFromText(content: string, name: string, type?: string): void { + const blob = new Blob([content], { type: type || "text/plain" }); + + const a = document.createElement("a"); + a.download = name; + a.href = window.URL.createObjectURL(blob); + a.click(); +} + +export function saveFileFromURL(url: string, name: string): void { + const a = document.createElement("a"); + a.download = name; + a.href = url; + a.click(); +} diff --git a/src/utils/number.test.ts b/src/utils/number.test.ts new file mode 100644 index 0000000..f9fa337 --- /dev/null +++ b/src/utils/number.test.ts @@ -0,0 +1,96 @@ +import { times } from "lodash"; +import { describe, expect, test } from "vitest"; + +import { findRanges, isNumber, shortenNumber } from "./number"; + +describe("Number utils", () => { + describe("#findRanges", () => { + test("should find around 10 steps", () => { + expect(findRanges(0.3, 9.7)).toStrictEqual({ unit: 1, ranges: times(10).map((n) => [n, n + 1]) }); + expect(findRanges(3, 97)).toStrictEqual({ unit: 10, ranges: times(10).map((n) => [n * 10, (n + 1) * 10]) }); + }); + + test("should look for a better 2 fit", () => { + expect(findRanges(0.3, 19.7)).toStrictEqual({ unit: 2, ranges: times(10).map((n) => [2 * n, 2 * (n + 1)]) }); + }); + + test("should look for the proper power of 10", () => { + expect(findRanges(0.03, 0.97)).toStrictEqual({ unit: 0.1, ranges: times(10).map((n) => [n / 10, (n + 1) / 10]) }); + }); + + test("should look for a better 2 or 5 fit, with the proper power of 10", () => { + expect(findRanges(10.03, 11.97)).toStrictEqual({ + unit: 0.2, + ranges: times(10).map((n) => [10 + n / 5, 10 + (n + 1) / 5]), + }); + }); + + test("should make an extra range if the max fits exactly the last range", () => { + expect(findRanges(0, 8).ranges).toStrictEqual(times(9).map((n) => [n, n + 1])); + expect(findRanges(0, 7.99).ranges).toStrictEqual(times(8).map((n) => [n, n + 1])); + }); + }); + + describe("#shortenNumber", () => { + test("should normally print small numbers", () => { + expect(shortenNumber(0)).toStrictEqual("0"); + expect(shortenNumber(1)).toStrictEqual("1"); + expect(shortenNumber(123)).toStrictEqual("123"); + expect(shortenNumber(1.23)).toStrictEqual("1.23"); + }); + + test("should work properly with small floats", () => { + expect(shortenNumber(0.0001)).toStrictEqual("0.0001"); + expect(shortenNumber(0.000123456)).toStrictEqual("0.000123"); + expect(shortenNumber(1.000123456)).toStrictEqual("1"); + expect(shortenNumber(1234.000123456)).toStrictEqual("1.2k"); + }); + + test("should work properly over 100", () => { + expect(shortenNumber(100)).toStrictEqual("100"); + expect(shortenNumber(1000)).toStrictEqual("1k"); + expect(shortenNumber(1234)).toStrictEqual("1.2k"); + expect(shortenNumber(12345)).toStrictEqual("12.3k"); + expect(shortenNumber(123456)).toStrictEqual("123.5k"); + expect(shortenNumber(1234567)).toStrictEqual("1.2m"); + }); + + test("should work properly with negative values", () => { + expect(shortenNumber(-1)).toStrictEqual("-1"); + expect(shortenNumber(-100)).toStrictEqual("-100"); + expect(shortenNumber(-1000)).toStrictEqual("-1k"); + expect(shortenNumber(-123)).toStrictEqual("-123"); + expect(shortenNumber(-1234)).toStrictEqual("-1.2k"); + expect(shortenNumber(-12345)).toStrictEqual("-12.3k"); + expect(shortenNumber(-123456)).toStrictEqual("-123.5k"); + expect(shortenNumber(-1234567)).toStrictEqual("-1.2m"); + expect(shortenNumber(-0.0001)).toStrictEqual("-0.0001"); + expect(shortenNumber(-0.000123456)).toStrictEqual("-0.000123"); + expect(shortenNumber(-1234.000123456)).toStrictEqual("-1.2k"); + }); + }); + + describe("#isNumber", () => { + test("should work with obvious cases", () => { + expect(isNumber(123)).toBe(true); + expect(isNumber(-123)).toBe(true); + expect(isNumber("123")).toBe(true); + expect(isNumber("-123")).toBe(true); + + expect(isNumber(false)).toBe(false); + expect(isNumber(null)).toBe(false); + expect(isNumber(undefined)).toBe(false); + expect(isNumber("abc")).toBe(false); + expect(isNumber({ abc: 123 })).toBe(false); + expect(isNumber([123])).toBe(false); + expect(isNumber(new Date())).toBe(false); + }); + + test("should properly handles strings that are not optimal numbers", () => { + expect(isNumber(" 123")).toBe(true); + expect(isNumber("123 ")).toBe(true); + expect(isNumber("123.00")).toBe(true); + expect(isNumber("000123")).toBe(true); + }); + }); +}); diff --git a/src/utils/number.ts b/src/utils/number.ts new file mode 100644 index 0000000..891d12f --- /dev/null +++ b/src/utils/number.ts @@ -0,0 +1,45 @@ +import { inRange, round } from "lodash"; + +export function findRanges(min: number, max: number): { unit: number; ranges: [number, number][] } { + if (max <= min) return { ranges: [[Math.min(min, max), Math.max(min, max)]], unit: Math.abs(max - min) }; + + const ranges: [number, number][] = []; + + const diff = max - min; + const digits = Math.floor(Math.log10(diff)) - 1; + const p = Math.pow(10, digits); + const unit = [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50].map((n) => n * p).find((n) => inRange(diff / n, 5, 15)); + + if (!unit) return { ranges: [[min, max]], unit: max - min }; + + for (let i = Math.floor(min / unit); i <= max / unit; i++) { + ranges.push([round(i * unit, -digits), round((i + 1) * unit, -digits)]); + } + + return { unit, ranges }; +} + +export function shortenNumber(n: number): string { + if (n === 0) return "0"; + if (n < 0) return "-" + shortenNumber(-n); + + const suffixes = ["", "k", "m", "b", "t"]; + const suffixNum = Math.floor(Math.log10(n) / 3); + const shortValue = suffixNum ? +(n / Math.pow(1000, suffixNum)).toFixed(2) : n; + + return suffixes[suffixNum] + ? (shortValue % 1 ? shortValue.toFixed(1) : shortValue) + suffixes[suffixNum] + : n + .toPrecision(3) + .replace(/(?:(\.0+[^0]+)|(\.[^0\d]*))0+$/g, "$1") + .replace(/\.$/g, ""); +} + +export function isNumber(v: unknown): boolean { + if (typeof v === "number") return true; + if (typeof v === "string") { + return !isNaN(+v); + } + + return false; +} diff --git a/src/utils/string.test.ts b/src/utils/string.test.ts new file mode 100644 index 0000000..615d77a --- /dev/null +++ b/src/utils/string.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, test } from "vitest"; + +import { minimize, normalize, slugify } from "./string"; + +describe("String utils", () => { + describe("#slugify", () => { + test("should replace characters that are not letters or digits by _s", () => { + expect(slugify("123abc456!*; lol")).toBe("123abc456_lol"); + }); + + test("should toggle to lower case", () => { + expect(slugify("LoreMIpSumdOLor")).toBe("loremipsumdolor"); + }); + + test("should clean accents", () => { + expect(slugify("pâté")).toBe("pate"); + }); + }); + + describe("#normalize", () => { + test("should not remove characters that are not letters", () => { + expect(normalize("123abc456!*; lol")).toBe("123abc456!*; lol"); + }); + + test("should toggle to lower case", () => { + expect(normalize("LoreMIpSumdOLor")).toBe("loremipsumdolor"); + }); + + test("should clean accents", () => { + expect(normalize("pâté")).toBe("pate"); + }); + }); + + describe("#minimize", () => { + test("should work with normal base case", () => { + expect(minimize(["john", "marius", "albert"].map(normalize))).toStrictEqual(["j", "m", "a"]); + }); + + test("should use two letters for similar words", () => { + expect(minimize(["john", "marius", "maxime"].map(normalize))).toStrictEqual(["j", "mr", "mx"]); + }); + + test("should use more than two letters when necessary", () => { + expect(minimize(["maxime", "marius", "marcellus"].map(normalize))).toStrictEqual(["mx", "mri", "mrc"]); + }); + + test("should use digits when letters don't differ enough", () => { + expect(minimize(["creme", "crème", "crémé"].map(normalize))).toStrictEqual(["c1", "c2", "c3"]); + }); + + test("should use additional letters AND digits when needed", () => { + expect(minimize(["cassis", "creme", "crème"].map(normalize))).toStrictEqual(["ca", "cr1", "cr2"]); + }); + }); +}); diff --git a/src/utils/string.ts b/src/utils/string.ts new file mode 100644 index 0000000..6e62ee3 --- /dev/null +++ b/src/utils/string.ts @@ -0,0 +1,58 @@ +import { forEach, groupBy, range, size } from "lodash"; + +/** + * Takes a string and returns a new string with only letters and strings, + * desaccentuated and lower-cased, and with other characters replaced by _s. + */ +export function slugify(string: string): string { + return string + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase() + .replace(/[^a-z0-9]+/gi, "_"); +} + +/** + * Takes a string and returns a desaccentuated and lower-cased new string. + */ +export function normalize(string: string): string { + return string + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase(); +} + +/** + * Takes an array of strings, and returns an array of those strings as minified + * as we could, taking the first letter, and adding letters as long as it's + * necessary (and digits in the end when words are too similar). + */ +function _minimize(strings: string[], __internal_nested__: boolean): string[] { + const minimized: Record = {}; + const indexedByFirstLetter = groupBy(range(strings.length), (index) => strings[index][0] || ""); + const injectLetter = !__internal_nested__ || size(indexedByFirstLetter) > 1; + + // Deal with characters with same first letter: + forEach(indexedByFirstLetter, (indices, firstLetter) => { + if (indices.length === 1) { + minimized[indices[0]] = firstLetter; + } else if (firstLetter) { + const superMinimized = _minimize( + indices.map((i) => strings[i].substring(1)), + true, + ); + indices.forEach((indexInStrings, indexInIndices) => { + minimized[indexInStrings] = (injectLetter ? firstLetter : "") + superMinimized[indexInIndices]; + }); + } else { + indices.forEach((indexInStrings, i) => { + minimized[indexInStrings] = i + 1 + ""; + }); + } + }); + + return strings.map((str, i) => minimized[i]); +} +export function minimize(strings: string[]): string[] { + return _minimize(strings, false); +} diff --git a/src/utils/threshold.test.ts b/src/utils/threshold.test.ts new file mode 100644 index 0000000..e5e5202 --- /dev/null +++ b/src/utils/threshold.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from "vitest"; + +import { MAX_LABEL_THRESHOLD, MIN_LABEL_THRESHOLD } from "../lib/consts"; +import { inputToStateThreshold, stateToInputThreshold } from "./threshold"; + +describe("Label thresholds translation utils", () => { + describe("#inputThresholdToStateThreshold", () => { + test("should work 'normally' with normal single values cases", () => { + expect(inputToStateThreshold(1)).toBe(6); + expect(inputToStateThreshold(2)).toBe(3); + expect(inputToStateThreshold(0.5)).toBe(12); + }); + + test("should work with extreme values", () => { + expect(inputToStateThreshold(MAX_LABEL_THRESHOLD)).toBe(0); + expect(inputToStateThreshold(MAX_LABEL_THRESHOLD + 0.001)).toBe(0); + expect(inputToStateThreshold(MIN_LABEL_THRESHOLD)).toBe(Infinity); + expect(inputToStateThreshold(MIN_LABEL_THRESHOLD - 0.001)).toBe(Infinity); + }); + }); + + describe("#stateThresholdToInputThreshold", () => { + test("should work 'normally' with normal single values cases", () => { + expect(stateToInputThreshold(6)).toBe(1); + expect(stateToInputThreshold(3)).toBe(2); + expect(stateToInputThreshold(12)).toBe(0.5); + }); + + test("should work with extreme values", () => { + expect(stateToInputThreshold(0)).toBe(MAX_LABEL_THRESHOLD); + expect(stateToInputThreshold(Infinity)).toBe(MIN_LABEL_THRESHOLD); + }); + }); +}); diff --git a/src/utils/threshold.ts b/src/utils/threshold.ts new file mode 100644 index 0000000..0fafdd7 --- /dev/null +++ b/src/utils/threshold.ts @@ -0,0 +1,13 @@ +import { MAX_LABEL_THRESHOLD, MIN_LABEL_THRESHOLD } from "../lib/consts"; + +export function stateToInputThreshold(v: number): number { + if (v === Infinity) return MIN_LABEL_THRESHOLD; + if (v === 0) return MAX_LABEL_THRESHOLD; + return 6 / v; +} + +export function inputToStateThreshold(v: number): number { + if (v <= MIN_LABEL_THRESHOLD) return Infinity; + if (v >= MAX_LABEL_THRESHOLD) return 0; + return 6 / v; +} diff --git a/src/utils/url.test.ts b/src/utils/url.test.ts new file mode 100644 index 0000000..4266fce --- /dev/null +++ b/src/utils/url.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, test } from "vitest"; + +import { buildURLSearchParams, queryStringToRecord, urlSearchParamsToString } from "./url"; + +describe("URL utils", () => { + describe("#buildURLSearchParams", () => { + test("should work 'normally' with normal single values cases", () => { + const data = { a: "abc", b: "def" }; + expect(buildURLSearchParams(data).toString()).toStrictEqual(new URLSearchParams(data).toString()); + }); + + test("should encode arrays using [] suffix", () => { + const data = { a: "abc", b: ["def", "ghi"] }; + const params = buildURLSearchParams(data); + expect([...params.getAll("a")]).toStrictEqual(["abc"]); + expect([...params.getAll("b")]).toStrictEqual(["def", "ghi"]); + }); + }); + + describe("#urlSearchParamsToString", () => { + test("should work 'normally' with normal single values cases", () => { + const params = new URLSearchParams(); + params.append("a", "abc"); + params.append("b", "def"); + expect(urlSearchParamsToString(params)).toBe(params.toString()); + }); + + test("should detect arrays", () => { + const params = new URLSearchParams(); + params.append("a", "abc"); + params.append("b", "def"); + params.append("b", "ghi"); + expect(urlSearchParamsToString(params)).toStrictEqual("a=abc&b[]=def&b[]=ghi"); + }); + }); + + describe("#queryStringToRecord", () => { + test("should work 'normally' with normal single values cases", () => { + expect(queryStringToRecord("a=abc&b=def")).toStrictEqual({ a: "abc", b: "def" }); + }); + + test("should detect arrays", () => { + expect(queryStringToRecord("a=abc&b[]=def&b[]=ghi")).toStrictEqual({ a: "abc", b: ["def", "ghi"] }); + }); + + test("should be flexible about the `[]` suffix", () => { + expect(queryStringToRecord("a=abc")).toStrictEqual({ a: "abc" }); + expect(queryStringToRecord("a[]=abc")).toStrictEqual({ a: "abc" }); + expect(queryStringToRecord("a=abc&a=def")).toStrictEqual({ a: ["abc", "def"] }); + expect(queryStringToRecord("a[]=abc&a[]=def")).toStrictEqual({ a: ["abc", "def"] }); + }); + }); +}); diff --git a/src/utils/url.ts b/src/utils/url.ts new file mode 100644 index 0000000..506f40f --- /dev/null +++ b/src/utils/url.ts @@ -0,0 +1,96 @@ +import { forEach } from "lodash"; + +/** + * Takes a URLSearchParams, and returns a valid query URL that handles keys with + * multiple values with the `"[]"` suffix, when URLSearchParams#toString does + * not. + * + * @example + * const params = new URLSearchParams(); + * params.append('age', '123'); + * params.append('hobby', 'soccer'); + * params.append('hobby', 'cooking'); + * + * console.log(params.toString()); + * // "age=123&hobby=soccer&hobby=cooking" + * + * console.log(urlSearchParamsToString(params)); + * // "age=123&hobby[]=soccer&hobby[]=cooking" + */ +export function urlSearchParamsToString(params: URLSearchParams): string { + const pairs = []; + const searchedKeys = new Set(); + + for (const key of params.keys()) { + if (searchedKeys.has(key)) continue; + searchedKeys.add(key); + + const values = params.getAll(key); + + if (values.length === 1) { + pairs.push([key, values[0]].map((s) => encodeURIComponent(s)).join("=")); + } else { + const fullKey = encodeURIComponent(key) + "[]"; + values.forEach((s) => pairs.push(`${fullKey}=${encodeURIComponent(s)}`)); + } + } + + return pairs.join("&"); +} + +/** + * Takes a record with strings or string arrays as values, and returns a new + * URLSearchParams instance that has all values for array values. + * + * @example + * const data = { hobby: ['soccer', 'cooking'] }; + * console.log(buildURLSearchParams(data).getAll('hobby')); + * // ['soccer', 'cooking'] + */ +export function buildURLSearchParams(data: Record): URLSearchParams { + const params = new URLSearchParams(); + + forEach(data, (value, key) => { + if (Array.isArray(value)) { + value.forEach((v) => { + params.append(key, v); + }); + } else { + params.append(key, value); + } + }); + + return params; +} + +/** + * Takes a query string and returns a properly parsed record, that handles array + * values with the `"[]"` prefix. + * + * @example + * const data = queryStringToRecord("age=123&hobby[]=soccer&hobby[]=cooking"); + * + * console.log(data.age); + * // "123" + * console.log(data.hobby); + * // ["soccer", "cooking"] + * console.log(data["hobby[]"]); + * // undefined + */ +export function queryStringToRecord(query: string): Record { + const params = new URLSearchParams(query); + + const data: Record = {}; + const searchedKeys = new Set(); + + for (const key of params.keys()) { + if (searchedKeys.has(key)) continue; + searchedKeys.add(key); + + const values = params.getAll(key); + + if (values.length) data[key.replace(/\[]$/, "")] = values.length === 1 ? values[0] : [...values]; + } + + return data; +} diff --git a/src/utils/useBlocker.ts b/src/utils/useBlocker.ts new file mode 100644 index 0000000..ef6cf9e --- /dev/null +++ b/src/utils/useBlocker.ts @@ -0,0 +1,33 @@ +/** + * React-router v6 lost a very interesting feature, which can block transitions from happening, for instance when a form + * is not confirmed. A ticket is opened to restore this feature (https://github.com/remix-run/react-router/issues/8139) + * and @rmorse wrote a fallback Gist here: https://gist.github.com/rmorse/426ffcc579922a82749934826fa9f743 + * This file is that fallback, but properly typed for TypeScript: + */ +import type { History, Transition } from "history"; +import { useContext, useEffect } from "react"; +import { UNSAFE_NavigationContext as NavigationContext } from "react-router-dom"; + +export declare type Navigator = Pick; + +export function useBlocker(blocker: (tx: Transition) => void, when = true) { + const navigator = useContext(NavigationContext).navigator as Navigator; + + useEffect(() => { + if (!when) return; + + const unblock = navigator.block((tx) => { + const autoUnblockingTx = { + ...tx, + retry() { + unblock(); + tx.retry(); + }, + }; + + blocker(autoUnblockingTx); + }); + + return unblock; + }, [navigator, blocker, when]); +} diff --git a/src/utils/useQueryParam.ts b/src/utils/useQueryParam.ts new file mode 100644 index 0000000..94eeb35 --- /dev/null +++ b/src/utils/useQueryParam.ts @@ -0,0 +1,71 @@ +import { useNavigate } from "react-router-dom"; + +type SetterInputFunction = (prev: Z) => Z; + +export function getQuery(): string { + return window.location.hash.replace(/^.*\?/, ""); +} + +function getQueryParams(): URLSearchParams { + const hash = window.location.hash; + if (hash.includes("?")) return new URLSearchParams(getQuery()); + return new URLSearchParams(); +} + +/** + * Hook to manage a variable in the url. + * + * @param key {string} Name of the variable + * @param defaultValue {string} The default / initial value of the attribute (not set in the url) + * @param read {function} A function to get the state value from the query value + * @param write {function} A function to get the query value from the state value + */ +export function useQueryParam( + key: string, + defaultValue: T, + read?: (queryValue: string | null) => T, + write?: (stateValue: T) => string | null, +): [T, (value: T | SetterInputFunction, replace?: boolean) => void] { + const navigate = useNavigate(); + + /** + * Retrieve the value of the given parameter. + */ + function getQueryParam(key: string): T { + const urlQueryParams = getQueryParams(); + const value = urlQueryParams.get(key); + + if (value === null) { + return defaultValue; + } + if (read) { + return read(value); + } + return value as unknown as T; + } + + /** + * Given a parameter, it returns the setter for it. + */ + function getSetQueryParam(key: string): (value: T | SetterInputFunction) => void { + return (value: T | SetterInputFunction, replace?: boolean): void => { + const urlQueryParams = getQueryParams(); + const prevValue = getQueryParam(key); + const newValue = typeof value === "function" ? (value as SetterInputFunction)(prevValue) : value; + + if (newValue !== prevValue) { + if (newValue !== defaultValue) { + const cleanedNewValue = write ? write(newValue) : newValue; + if (cleanedNewValue) urlQueryParams.set(key, cleanedNewValue + ""); + else urlQueryParams.delete(key); + } else { + urlQueryParams.delete(key); + } + + navigate({ search: `?${urlQueryParams.toString()}` }, { replace }); + } + }; + } + + return [getQueryParam(key), getSetQueryParam(key)]; +} diff --git a/src/utils/useTimeout.ts b/src/utils/useTimeout.ts new file mode 100644 index 0000000..04890ed --- /dev/null +++ b/src/utils/useTimeout.ts @@ -0,0 +1,36 @@ +import { useCallback, useEffect, useRef } from "react"; + +function isTimeoutValid(timeout: number): boolean { + return !isNaN(timeout) && timeout >= 0 && timeout !== Infinity; +} + +/** + * React hook for delaying calls with time. + * returns callback to use for cancelling + */ +export const useTimeout = ( + callback: () => void, + timeout: number = 0, +): { cancel: () => void; reschedule: () => void } => { + const timeoutIdRef = useRef(null); + + const cancel = useCallback(() => { + const timeoutId = timeoutIdRef.current; + if (timeoutId) { + timeoutIdRef.current = null; + clearTimeout(timeoutId); + } + }, [timeoutIdRef]); + + const reschedule = useCallback(() => { + cancel(); + timeoutIdRef.current = isTimeoutValid(timeout) ? window.setTimeout(callback, timeout) : null; + }, [callback, timeout, cancel]); + + useEffect(() => { + timeoutIdRef.current = isTimeoutValid(timeout) ? window.setTimeout(callback, timeout) : null; + return cancel; + }, [callback, timeout, cancel]); + + return { cancel, reschedule }; +}; diff --git a/src/views/ContextPanel.tsx b/src/views/ContextPanel.tsx new file mode 100644 index 0000000..64a4e5b --- /dev/null +++ b/src/views/ContextPanel.tsx @@ -0,0 +1,94 @@ +import cx from "classnames"; +import React, { FC, JSX, useContext, useMemo } from "react"; +import { BsShare } from "react-icons/bs"; +import { FaHome } from "react-icons/fa"; +import { MdOutlinePreview } from "react-icons/md"; +import { VscSettings } from "react-icons/vsc"; +import { Link } from "react-router-dom"; + +import Footer from "../components/Footer"; +import { GraphContext } from "../lib/context"; +import Filters from "./Filters"; +import GraphSumUp from "./GraphSumUp"; +import NodesAppearanceBlock from "./NodesAppearanceBlock"; +import ReadabilityBlock from "./ReadabilityBlock"; +import SelectedNodePanel from "./SelectedNodePanel"; + +const ContextPanel: FC = () => { + const { navState, embedMode, data, panel, setPanel, openModal } = useContext(GraphContext); + + const selectedNode = useMemo( + () => + navState?.selectedNode && data?.graph.hasNode(navState.selectedNode) + ? data.graph.getNodeAttributes(navState.selectedNode) + : null, + [data?.graph, navState?.selectedNode], + ); + + let content: JSX.Element; + if (panel === "readability") { + content = ; + } else if (selectedNode) { + content = ; + } else { + content = ( + <> + + + + + ); + } + + const selectedButtonClass = "btn-dark opacity-100"; + + return ( +
+
+
+ + + + + + {!embedMode && ( + + )} + + + + +
+
+ +
+
{content}
+ +
+
+
+
+
+
+ ); +}; + +export default ContextPanel; diff --git a/src/views/EditionPanel.tsx b/src/views/EditionPanel.tsx new file mode 100644 index 0000000..e8a456e --- /dev/null +++ b/src/views/EditionPanel.tsx @@ -0,0 +1,339 @@ +import cx from "classnames"; +import { keyBy, pull, uniqBy } from "lodash"; +import React, { FC, JSX, useContext, useMemo } from "react"; +import { BiSolidNetworkChart } from "react-icons/bi"; +import { BsPaletteFill, BsShare } from "react-icons/bs"; +import { FaTimes } from "react-icons/fa"; +import { MdBubbleChart } from "react-icons/md"; +import { RiFilterFill } from "react-icons/ri"; +import { VscSettings } from "react-icons/vsc"; +import Select from "react-select"; + +import { DEFAULT_SELECT_PROPS } from "../lib/consts"; +import { GraphContext } from "../lib/context"; +import { + DEFAULT_EDGE_COLORING, + DEFAULT_EDGE_DIRECTION, + EDGE_COLORING_MODES, + EDGE_DIRECTION_MODES, + EdgeColoring, + EdgeDirection, + NavState, +} from "../lib/navState"; + +const EDGE_COLORING_LABELS: Record = { + s:
Use source node color
, + t:
Use target node color
, + o:
Use original color
, + c: ( +
+ Color all edges as grey +
+ (can be useful to keep the user focus on nodes) +
+
+ ), +}; + +const EDGE_DIRECTION_LABELS: Record = { + o:
Trust the original graph file
, + d: ( +
+ All edges should be treated as directed +
+ ), + u: ( +
+ All edges should be treated as undirected +
+ ), +}; + +interface Option { + value: string; + label: string; + field?: string; +} + +const EditionPanel: FC<{ isExpanded: boolean }> = ({ isExpanded }) => { + const { navState, data, setNavState, openModal, setPanel } = useContext(GraphContext); + const { fields, fieldsIndex } = data; + const { filterable, colorable, sizeable, subtitleFields } = navState; + + const edgeColoring = navState.edgeColoring || DEFAULT_EDGE_COLORING; + const edgeDirection = navState.edgeDirection || DEFAULT_EDGE_DIRECTION; + + const sizeableSet = new Set(sizeable); + const colorableSet = new Set(colorable); + const filterableSet = new Set(filterable); + + const sets: Record> = { + sizeable: sizeableSet, + colorable: colorableSet, + filterable: filterableSet, + }; + + const subtitleOptions: Option[] = useMemo( + () => + uniqBy( + fields.map((key) => { + const field = fieldsIndex[key]; + return { + value: `${key}-field`, + label: field.label, + field: key, + }; + }), + ({ field }) => fieldsIndex[field].rawFieldId, + ), + [fields, fieldsIndex], + ); + const optionsIndex = keyBy(subtitleOptions, "field"); + const selectedOptions = (subtitleFields || []).map((f) => optionsIndex[f]); + + return ( +
+
+
+
+ + +

+ DeepGit logo + Welcome to DeepGit +

+ +

+ Before sharing your graph online, you can first select various options on how users will{" "} + read and interrogate this graph. +

+

+ Once you're done, simply close this panel. You will be able to access this form again later, from the{" "} + {" "} + panel. +

+ {!navState.local && ( +

+ PS: This panel is only here to help you configure DeepGit. Unless you specifically want it to be, it will + not be visible to the users you share your graph with, if you click the{" "} + {" "} + button to share or embed this graph. +

+ )} + +
+ +
+ +
+

Which fields should be actionable?

+ + + + + + + + + + + + {fields.map((f) => { + const field = fieldsIndex[f]; + + return ( + + {["filterable", "colorable", "sizeable"].map((key) => { + const colorOrSize = sizeableSet.has(f) || colorableSet.has(f); + const disabled = + (key === "filterable" && colorOrSize) || + (key === "sizeable" && field.type !== "quanti") || + (key === "colorable" && field.type !== "quali" && field.type !== "quanti"); + const checked = sets[key].has(f) || (key === "filterable" && colorOrSize); + const keyToUpdate = { + sizeable: "size", + colorable: "color", + }[key]; + + return ( + + ); + })} + + + ); + })} + + + ); + })} + + + +
+ + Filter + + + + Colors + + + + Sizes + + +
+ + setNavState({ + ...navState, + [key]: e.target.checked + ? ((navState as any)[key] || []).concat(f) + : pull((navState as any)[key] || [], f), + ...(e.target.checked && keyToUpdate ? { [keyToUpdate]: f } : {}), + }) + } + /> + + {field.label} + {field.typeLabel &&
{field.typeLabel}
} +
+ {["colorable", "sizeable"].map((key) => { + const keyToUpdate = ( + { + sizeable: "disableDefaultSize", + colorable: "disableDefaultColor", + } as Record + )[key]; + const disabled = ((navState as any)[key] || []).length < 1; + const checked = !navState[keyToUpdate]; + + return ( + + setNavState({ ...navState, [keyToUpdate]: !e.target.checked })} + /> + Allow using default graph file colors and/or sizes
+
+ +
+ + setValue(e.target.value)} + /> + + ); +}; + +const TermsFilterComponent: FC<{ + field: QualiField; + data: TermsMetric; + filter?: TermsFilter | undefined; + setFilter: (filter: TermsFilter | null) => void; + getColor?: ((value: any) => string) | null; + editable: boolean; +}> = ({ field, data, filter, setFilter, getColor, editable }) => { + const { data: graphData, setHovered } = useContext(GraphContext); + const [alphaSort, setAlphaSort] = useState(false); + const [expanded, setExpanded] = useState(false); + const maxCount = max(data.values.map((v) => v.globalCount)) as number; + const filteredValues = filter?.values ? new Set(filter.values) : null; + + const showExpandToggle = data.values.length > MAX_PALETTE_SIZE; + const values = useMemo( + () => + take( + alphaSort ? sortBy(data.values, (value) => value.label.toLowerCase()) : data.values, + showExpandToggle && !expanded ? MAX_PALETTE_SIZE : Infinity, + ), + [showExpandToggle, expanded, alphaSort, data.values], + ); + + return ( + <> +
+ +
+
    + {values.map((v) => { + const id = `terms-filters-${field.id}-${v.id}`; + const state = !filteredValues ? "idle" : filteredValues.has(v.id) ? "checked" : "unchecked"; + + return ( +
  • { + if (!editable) return; + + const checkedValues = filter ? filter.values : data.values.map((v) => v.id); + const newValues = + state === "idle" + ? [v.id] + : state === "unchecked" + ? checkedValues.concat(v.id) + : checkedValues.filter((s) => s !== v.id); + + if (newValues.length) { + setFilter({ + field: field.id, + type: "terms", + values: newValues, + }); + } else { + setFilter(null); + } + }} + onMouseEnter={() => { + setHovered( + new Set(graphData.graph.filterNodes((node, nodeData) => getValue(nodeData, field) === v.id)), + ); + }} + onMouseLeave={() => { + setHovered(undefined); + }} + > +
    + {state === "idle" ? ( + + ) : state === "checked" ? ( + + ) : ( + + )} + + {v.label}{" "} + {v.filteredCount > 0 && ( + + ({v.filteredCount} node{v.filteredCount > 1 ? "s" : ""}) + + )} + +
    +
    +
    +
    +
    +
  • + ); + })} +
+ {showExpandToggle && ( + + )} + + ); +}; + +const RangeFilterComponent: FC<{ + field: QuantiField; + data: RangeMetric; + filter?: RangeFilter | undefined; + setFilter: (filter: RangeFilter | null) => void; + getColor?: ((value: any) => string) | null; + editable: boolean; +}> = ({ field, data, filter, setFilter, getColor, editable }) => { + const { ranges, unit } = data; + const absMin = first(ranges)!.min; + const absMax = last(ranges)!.max; + const currentMin = typeof filter?.min === "number" ? filter.min : absMin; + const currentMax = typeof filter?.max === "number" ? filter.max : absMax; + const maxCount = Math.max(...ranges.map((r) => r.globalCount)); + const marks: SliderProps["marks"] = mapValues( + keyBy(uniq(ranges.flatMap((r) => [r.min, r.max]).concat([currentMin, currentMax]))), + (v) => + v === currentMin || v === currentMax + ? { + label: shortenNumber(v), + style: { fontWeight: "bold", background: "white", padding: "0 0.2em", zIndex: 1 }, + } + : shortenNumber(v), + ); + const [inputValues, setInputValues] = useState<{ min: number; max: number }>({ min: currentMin, max: currentMax }); + + useEffect(() => { + setInputValues({ min: currentMin, max: currentMax }); + }, [currentMin, currentMax]); + + const updateFilter = ([min, max]: number[]) => { + const newMin = min <= absMin ? undefined : min; + const newMax = max >= absMax ? undefined : max; + + if (!isNumber(newMin) && !isNumber(newMax)) { + setFilter(null); + } else { + const newFilter: RangeFilter = { + field: field.id, + type: "range", + }; + if (isNumber(newMin)) newFilter.min = newMin; + if (isNumber(newMax)) newFilter.max = newMax; + + setFilter(newFilter); + } + }; + + return ( +
+
    + {ranges.map((range) => { + const filteredHeight = (range.filteredCount / maxCount) * 100; + const isLabelInside = filteredHeight > 90; + const bgColor = getColor ? getColor(mean([range.min, range.max])) : "#343a40"; + const fontColor = getFontColor(bgColor); + + return ( +
    +
    +
    + {!!range.filteredCount && ( + + {shortenNumber(range.filteredCount)} + + )} +
    +
    + ); + })} +
+ + + + {editable && ( +
{ + e.preventDefault(); + updateFilter([inputValues.min, inputValues.max]); + }} + > + + Filter from{" "} + setInputValues((o) => ({ ...o, min: +e.target.value }))} + />{" "} + to{" "} + setInputValues((o) => ({ ...o, max: +e.target.value }))} + /> + + +
+ )} +
+ ); +}; + +const FilterWrapper: FC<{ + title: string; + subtitle?: string; + clearFilter?: () => void; + isColorField?: boolean; + isSizeField?: boolean; + children?: React.ReactNode; +}> = ({ title, subtitle, clearFilter, children, isColorField, isSizeField }) => { + const [expanded, setExpanded] = useState(!!isColorField || !!clearFilter); + + return ( +
+

+
+ {isColorField && } + {isSizeField && } +
+ {title} {subtitle &&
{subtitle}
} +
+
+ + + +

+ + + {children} + +
+ ); +}; + +const Filters: FC = () => { + const { navState, data, computedData, setNavState } = useContext(GraphContext); + const editable = navState.role !== "v"; + + const setFilters = useCallback( + (filters: Filter[] | null) => setNavState({ ...navState, filters: filters || undefined }), + [navState, setNavState], + ); + + const { getColor } = computedData; + const { filters, nodeColorField, nodeSizeField } = navState; + const filtersIndex = keyBy(filters || [], "field"); + + const allFilterable = getFilterableFields(data, navState); + + const cleanAndSetFilters = (field: string, filter: Filter | null) => { + const newFilters = filter + ? (filters || []).filter((f) => f.field !== field).concat(filter) + : (filters || []).filter((f) => f.field !== field); + + setFilters(newFilters.length ? newFilters : null); + }; + + if (!allFilterable.length) return null; + + return ( +
+ {allFilterable.map((field) => { + const metric = computedData.metrics[field.id]; + + if (!metric || !field) return null; + + switch (field.type) { + case "quanti": + return ( + cleanAndSetFilters(field.id, null) : undefined} + isColorField={field.id === nodeColorField} + isSizeField={field.id === nodeSizeField} + > + cleanAndSetFilters(field.id, filter)} + getColor={field.id === nodeColorField ? getColor : null} + editable={editable} + /> + + ); + case "quali": + return ( + cleanAndSetFilters(field.id, null) : undefined} + isColorField={field.id === nodeColorField} + isSizeField={field.id === nodeSizeField} + > + cleanAndSetFilters(field.id, filter)} + getColor={field.id === nodeColorField ? getColor : null} + editable={editable} + /> + + ); + case "content": + default: + return editable ? ( + cleanAndSetFilters(field.id, null) : undefined} + isColorField={field.id === nodeColorField} + isSizeField={field.id === nodeSizeField} + > + cleanAndSetFilters(field.id, filter)} + /> + + ) : null; + } + })} +
+ ); +}; + +export default Filters; diff --git a/src/views/GraphAppearance.tsx b/src/views/GraphAppearance.tsx new file mode 100644 index 0000000..19b216a --- /dev/null +++ b/src/views/GraphAppearance.tsx @@ -0,0 +1,95 @@ +import { useSigma } from "@react-sigma/core"; +import React, { FC, useContext, useEffect, useState } from "react"; +import { DEFAULT_SETTINGS } from "sigma/settings"; + +import { LoaderFill } from "../components/Loader"; +import { DEFAULT_LABEL_THRESHOLD } from "../lib/consts"; +import { GraphContext } from "../lib/context"; +import { + applyEdgeColors, + applyEdgeDirections, + applyEdgeSizes, + applyNodeColors, + applyNodeLabelSizes, + applyNodeSizes, + applyNodeSubtitles, + getReducers, +} from "../lib/graph"; +import drawLabel, { drawHover } from "../utils/canvas"; +import { inputToStateThreshold } from "../utils/threshold"; + +const GraphAppearance: FC = () => { + const { data, navState, computedData, setSigma, hovered } = useContext(GraphContext); + const { + nodeSizeField, + minLabelSize, + maxLabelSize, + subtitleFields, + nodeSizeRatio, + edgeSizeRatio, + edgeColoring, + edgeDirection, + } = navState; + const labelThreshold = inputToStateThreshold(navState.labelThresholdRatio || DEFAULT_LABEL_THRESHOLD); + const { nodeColors, nodeSizes, edgeSizes, nodeSizeExtents } = computedData; + const sigma = useSigma(); + + const [isRendered, setIsRendered] = useState(false); + + useEffect(() => { + setSigma(sigma); + sigma.setSetting("defaultDrawNodeLabel", (context, data, settings) => + drawLabel(context, { ...sigma.getNodeDisplayData(data.key), ...data }, settings), + ); + sigma.setSetting("defaultDrawNodeHover", (context, data, settings) => + drawHover(context, { ...sigma.getNodeDisplayData(data.key), ...data }, settings), + ); + + return () => setSigma(undefined); + }, [sigma, setSigma]); + + useEffect(() => { + const { node, edge } = getReducers(data, navState, computedData, hovered); + sigma.setSetting("nodeReducer", node); + sigma.setSetting("edgeReducer", edge); + setIsRendered(true); + }, [data, navState, computedData, hovered, sigma]); + + useEffect(() => { + const labelDensity = labelThreshold === 0 ? Infinity : DEFAULT_SETTINGS.labelDensity; + sigma.setSetting("labelRenderedSizeThreshold", labelThreshold); + sigma.setSetting("labelDensity", labelDensity); + }, [labelThreshold, sigma]); + + useEffect(() => { + applyNodeColors(data, { nodeColors }); + }, [sigma, data, nodeColors]); + + useEffect(() => { + applyNodeSizes(data, { nodeSizes }, { nodeSizeRatio }); + }, [sigma, data, nodeSizeRatio, nodeSizes]); + + useEffect(() => { + applyNodeLabelSizes(data, { nodeSizeExtents }, { nodeSizeField, minLabelSize, maxLabelSize }); + }, [sigma, data, nodeSizeField, minLabelSize, maxLabelSize, nodeSizeExtents]); + + useEffect(() => { + applyNodeSubtitles(data, { subtitleFields }); + }, [sigma, data, subtitleFields]); + + useEffect(() => { + applyEdgeColors(data, { nodeColors }, { edgeColoring }); + }, [sigma, data, nodeColors, edgeColoring]); + + useEffect(() => { + applyEdgeDirections(data, { edgeDirection }); + }, [sigma, data, edgeDirection]); + + useEffect(() => { + applyEdgeSizes(data, { edgeSizes }, { edgeSizeRatio }); + }, [sigma, data, edgeSizes, edgeSizeRatio]); + + return isRendered ? null : ; +}; + +export default GraphAppearance; diff --git a/src/views/GraphControls.tsx b/src/views/GraphControls.tsx new file mode 100644 index 0000000..844f563 --- /dev/null +++ b/src/views/GraphControls.tsx @@ -0,0 +1,224 @@ +import { useSigma } from "@react-sigma/core"; +import { downloadAsPNG } from "@sigma/export-image"; +import cx from "classnames"; +import { keyBy, take } from "lodash"; +import React, { FC, useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { BiRadioCircleMarked } from "react-icons/bi"; +import { BsSearch, BsZoomIn, BsZoomOut } from "react-icons/bs"; +import { FaFileImage } from "react-icons/fa"; +import { OptionProps } from "react-select"; +import AsyncSelect from "react-select/async"; +import { Coordinates } from "sigma/types"; + +import Node from "../components/Node"; +import { ANIMATION_DURATION, DEFAULT_SELECT_PROPS, MAX_OPTIONS, RETINA_FIELD_PREFIX } from "../lib/consts"; +import { AppContext, GraphContext } from "../lib/context"; +import { NodeData } from "../lib/data"; +import { normalize, slugify } from "../utils/string"; +import GraphFullScreenControl from "./GraphFullScreenControl"; + +const TYPE_NODE = "node" as const; +const TYPE_MESSAGE = "message" as const; + +interface NodeOption { + type: typeof TYPE_NODE; + value: string; + label: string; + node: NodeData; +} +interface MessageOption { + type: typeof TYPE_MESSAGE; + value: string; + label: string; + isDisabled: true; +} +type Option = NodeOption | MessageOption; + +function cropOptions(options: Option[]): Option[] { + const moreOptionsCount = options.length - MAX_OPTIONS; + return moreOptionsCount > 1 + ? take(options, MAX_OPTIONS).concat({ + type: TYPE_MESSAGE, + value: RETINA_FIELD_PREFIX + "more-values", + label: `...and ${moreOptionsCount > 1 ? moreOptionsCount + " more nodes" : "one more node"}`, + isDisabled: true, + }) + : options; +} + +function doesMatch(normalizedQuery: string, searchableNormalizedStrings: string[]): boolean { + return searchableNormalizedStrings.some((str) => str.includes(normalizedQuery)); +} + +const OptionComponent = ({ data, innerProps, className, isFocused }: OptionProps) => { + return ( +
+ {data.type === TYPE_NODE && ( + + )} + {data.type === TYPE_MESSAGE &&
{data.label}
} +
+ ); +}; + +const IndicatorComponent = () => { + return ( +
+ +
+ ); +}; + +const GraphSearch: FC = () => { + const sigma = useSigma(); + const { portalTarget } = useContext(AppContext); + const { + setNavState, + navState, + data, + computedData: { filteredNodes }, + } = useContext(GraphContext); + const [nodesIndex, setNodesIndex] = useState>({}); + + // Index nodes on mount: + useEffect(() => { + setNodesIndex( + data.graph.reduceNodes( + (iter, node, attributes) => ({ + ...iter, + [node]: [normalize(node), normalize(attributes.label)], + }), + {}, + ), + ); + }, [data.graph]); + + const options: Option[] = useMemo( + () => + data.graph + .mapNodes((node, attributes) => { + return { + type: TYPE_NODE, + value: node, + label: attributes.label, + node: attributes, + }; + }) + .filter((n) => !filteredNodes || filteredNodes.has(n.value)), + [data.graph, filteredNodes], + ); + const firstOptions = useMemo(() => cropOptions(options), [options]); + const optionsSet = keyBy(options, "value"); + const selectNode = useCallback( + (option: Option | null) => { + if (!option) { + setNavState({ ...navState, selectedNode: undefined }); + } else { + setNavState({ ...navState, selectedNode: option.value }); + const nodePosition = sigma.getNodeDisplayData(option.value) as Coordinates; + sigma.getCamera().animate( + { ...nodePosition, ratio: 0.5 }, + { + duration: ANIMATION_DURATION, + }, + ); + } + }, + [navState, setNavState, sigma], + ); + const filterOptions = useCallback( + (query: string, callback: (options: Option[]) => void) => { + const normalizedQuery = normalize(query); + callback( + cropOptions( + options.filter( + (option) => option.type === TYPE_NODE && doesMatch(normalizedQuery, nodesIndex[option.value] || []), + ), + ), + ); + }, + [nodesIndex, options], + ); + + return ( + + {...DEFAULT_SELECT_PROPS} + isClearable + menuPortalTarget={portalTarget} + className="mb-2" + placeholder="Search for nodes..." + defaultOptions={firstOptions} + loadOptions={filterOptions} + value={navState.selectedNode ? optionsSet[navState.selectedNode] || null : null} + onChange={(option: Option | null) => selectNode(option?.type === TYPE_NODE ? option : null)} + components={{ + Option: OptionComponent, + DropdownIndicator: IndicatorComponent, + }} + styles={{ + control: (styles) => { + return { + ...styles, + width: "200px", + }; + }, + }} + /> + ); +}; + +const GraphControls: FC = () => { + const sigma = useSigma(); + const graph = sigma.getGraph(); + + const zoom = useCallback( + (ratio?: number): void => { + if (sigma) { + if (!ratio) { + sigma.getCamera().animatedReset({ duration: ANIMATION_DURATION }); + } else if (ratio > 0) { + sigma.getCamera().animatedZoom({ duration: ANIMATION_DURATION, factor: 1.5 }); + } else if (ratio < 0) { + sigma.getCamera().animatedUnzoom({ duration: ANIMATION_DURATION, factor: 1.5 }); + } + } + }, + [sigma], + ); + + const downloadImage = useCallback(() => { + const slug = slugify(graph.getAttribute("title") || "graph"); + downloadAsPNG(sigma, { + fileName: slug, + backgroundColor: "white", + }); + }, [graph, sigma]); + + return ( + <> + + + + + + + + + + + ); +}; + +export default GraphControls; diff --git a/src/views/GraphFullScreenControl.tsx b/src/views/GraphFullScreenControl.tsx new file mode 100644 index 0000000..2cc642f --- /dev/null +++ b/src/views/GraphFullScreenControl.tsx @@ -0,0 +1,43 @@ +import React, { FC, useCallback, useContext, useEffect, useState } from "react"; +import { BsArrowsFullscreen, BsFullscreenExit } from "react-icons/bs"; + +import { GraphContext } from "../lib/context"; + +function toggleFullScreen(dom: HTMLElement) { + if (!document.fullscreenEnabled) return; + + if (document.fullscreenElement !== dom) { + dom.requestFullscreen(); + } else { + document.exitFullscreen(); + } +} + +const GraphFullScreenControl: FC = () => { + const { root } = useContext(GraphContext); + + const [isFullScreen, setFullScreen] = useState(false); + const refreshState = useCallback(() => { + const isFullScreen = !!root && document.fullscreenElement === root; + setFullScreen(isFullScreen); + }, [root]); + + useEffect(() => { + document.addEventListener("fullscreenchange", refreshState); + return () => document.removeEventListener("fullscreenchange", refreshState); + }, [refreshState]); + + if (!document.fullscreenEnabled) return null; + + return ( + + ); +}; + +export default GraphFullScreenControl; diff --git a/src/views/GraphSumUp.tsx b/src/views/GraphSumUp.tsx new file mode 100644 index 0000000..714a52d --- /dev/null +++ b/src/views/GraphSumUp.tsx @@ -0,0 +1,105 @@ +import { map, startCase } from "lodash"; +import React, { FC, useCallback, useContext, useMemo } from "react"; +import { BiNetworkChart } from "react-icons/bi"; +import { FaFileDownload } from "react-icons/fa"; +import { MdOutlineOpenInNew } from "react-icons/md"; +import { RiFilterOffFill } from "react-icons/ri"; +import Linkify from "react-linkify"; + +import { DEFAULT_LINKIFY_PROPS } from "../lib/consts"; +import { GraphContext } from "../lib/context"; +import { Data } from "../lib/data"; +import { cleanNavState, navStateToQueryURL } from "../lib/navState"; +import { saveFileFromURL } from "../utils/file"; + +const GraphSumUp: FC = () => { + const { origin, pathname } = window.location; + const { embedMode, navState, data, computedData, setNavState } = useContext(GraphContext); + + const { graph } = data; + const attributes = useMemo(() => graph.getAttributes(), [graph]); + const { filteredNodes, filteredEdges } = computedData; + + const nodesTotal = graph.order; + const edgesTotal = graph.size; + const nodesVisible = filteredNodes ? filteredNodes.size : nodesTotal; + const edgesVisible = filteredEdges ? filteredEdges.size : edgesTotal; + const hasFilter = nodesVisible < nodesTotal; + + const downloadData = useCallback(() => { + const url = navState.url || ""; + saveFileFromURL(url, url.replace(/(^.*[\\/]|[?#].*$)/g, "")); + }, [navState.url]); + + const graphURL = useMemo(() => { + return origin + pathname + `#/graph?` + navStateToQueryURL(cleanNavState(navState, data as Data)); + }, [data, navState, origin, pathname]); + + return ( +
+

+ Graph overview +

+ +
+ + {navState.showGraphMeta && ( + <> + {map(attributes, (value, key) => ( +

+ {startCase(key)}:{" "} + + {value} + +

+ ))} + +
+ + )} + +

+ {nodesVisible.toLocaleString()} node{nodesVisible > 1 ? "s" : ""} + {hasFilter ? ( + {((nodesVisible / nodesTotal) * 100).toFixed(1)}% of full graph + ) : null} +

+

+ {edgesVisible.toLocaleString()} edge{edgesVisible > 1 ? "s" : ""} + {hasFilter ? ( + {((edgesVisible / edgesTotal) * 100).toFixed(1)}% of full graph + ) : null} +

+ +
+ +
+ {embedMode && ( + + + + )} + + {navState.role !== "v" && ( + + )} +
+
+ ); +}; + +export default GraphSumUp; diff --git a/src/views/GraphView.tsx b/src/views/GraphView.tsx new file mode 100644 index 0000000..c5ac23e --- /dev/null +++ b/src/views/GraphView.tsx @@ -0,0 +1,322 @@ +import { SigmaContainer } from "@react-sigma/core"; +import cx from "classnames"; +import React, { FC, createElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { BsChevronDoubleLeft, BsChevronDoubleRight } from "react-icons/bs"; +import { useLocation, useNavigate } from "react-router"; +import Sigma from "sigma"; +import { Dimensions } from "sigma/types"; + +import { LoaderFill } from "../components/Loader"; +import { + ComputedData, + getEdgeSizes, + getEmptyComputedData, + getMetrics, + getNodeColors, + getNodeSizes, +} from "../lib/computedData"; +import { BASE_SIGMA_SETTINGS } from "../lib/consts"; +import { GraphContext, Panel } from "../lib/context"; +import { Data, enrichData, loadGraphFile, loadGraphURL, prepareGraph, readGraph } from "../lib/data"; +import { + BAD_FILE, + BAD_URL, + MISSING_FILE, + MISSING_URL, + UNKNOWN, + getErrorMessage, + getReportNotification, +} from "../lib/errors"; +import { applyGraphStyle } from "../lib/graph"; +import { + DEFAULT_ROLE, + NavState, + cleanNavState, + guessNavState, + navStateToQueryURL, + queryURLToNavState, +} from "../lib/navState"; +import { useNotifications } from "../lib/notifications"; +import ContextPanel from "./ContextPanel"; +import EditionPanel from "./EditionPanel"; +import EventsController from "./EventsController"; +import GraphAppearance from "./GraphAppearance"; +import GraphControls from "./GraphControls"; +import LocalWarningBanner from "./LocalWarningBanner"; +import NodeSizeCaption from "./NodeSizeCaption"; +import { MODALS, ModalName } from "./modals"; + +const GraphView: FC<{ embed?: boolean }> = ({ embed = false }) => { + const navigate = useNavigate(); + const location = useLocation(); + const { notify } = useNotifications(); + const [ready, setReady] = useState(true); // set default value as `!embed` to get an overlay + + const state = location.state as { file?: unknown; fromHome?: unknown } | undefined; + const localFile = useMemo(() => (state?.file instanceof File ? state.file : null), [state]); + const fromHome = useMemo(() => !!state?.fromHome, [state]); + + const domRoot = useRef(null); + const [sigma, setSigma] = useState(undefined); + const [dimensions, setDimensions] = useState({ width: 1000, height: 1000 }); + const [hovered, setHovered] = useState | undefined>(undefined); + const [graphFile, setGraphFile] = useState<{ + name: string; + extension: string; + textContent: string; + } | null>(null); + const [data, setData] = useState(null); + const rawNavState = useMemo(() => queryURLToNavState(location.search), [location.search]); + + const url = useMemo(() => rawNavState.url, [rawNavState]); + const local = useMemo(() => rawNavState.local, [rawNavState]); + const navState = useMemo(() => (data ? cleanNavState(rawNavState, data) : null), [rawNavState, data]); + const setNavState = useCallback( + (newNavState: NavState) => { + navigate( + location.hash.replace(/^#/, "").replace(/\?.*/, "") + + "?" + + navStateToQueryURL(data ? cleanNavState(newNavState, data) : newNavState), + ); + }, + [data, location.hash, navigate], + ); + + const [modalName, setModalName] = useState(undefined); + const [panel, setPanel] = useState("main"); + const [isPanelExpanded, setIsPanelExpanded] = useState(!embed && navState?.role !== "d"); + + const [computedData, setComputedData] = useState(null); + + // Refresh aggregations and filtered items lists: + useEffect(() => { + if (data) { + setComputedData((old) => ({ + nodeSizes: {}, + edgeSizes: {}, + nodeSizeExtents: [0, Infinity], + edgeSizeExtents: [0, Infinity], + ...old, + ...getMetrics( + data, + { + filters: navState?.filters, + filterable: navState?.filterable, + colorable: navState?.colorable, + sizeable: navState?.sizeable, + }, + old?.metrics, + ), + })); + } + }, [sigma, data, navState?.filters, navState?.filterable, navState?.colorable, navState?.sizeable]); + + // On first computedData update, apply graph style: + useEffect(() => { + if (data && computedData && navState && !sigma) { + applyGraphStyle(data, computedData, navState); + } + }, [sigma, data, navState, computedData]); + + // Keep dimensions up to date: + useEffect(() => { + if (!sigma) return; + + const handler = () => setDimensions(sigma.getDimensions()); + sigma.on("resize", handler); + return () => { + sigma.off("resize", handler); + }; + }, [sigma]); + + // Refresh node colors and sizes: + useEffect(() => { + if (data) { + setComputedData((current) => ({ + ...(current || getEmptyComputedData()), + ...getNodeColors(data, { nodeColorField: navState?.nodeColorField }), + })); + } + }, [data, navState?.nodeColorField]); + useEffect(() => { + if (data) { + setComputedData((current) => ({ + ...(current || getEmptyComputedData()), + ...getNodeSizes( + data, + { nodeSizeField: navState?.nodeSizeField, nodeSizeRatio: navState?.nodeSizeRatio }, + dimensions, + ), + })); + } + }, [data, navState?.nodeSizeField, navState?.nodeSizeRatio, dimensions]); + useEffect(() => { + if (data) { + setComputedData((current) => ({ + ...(current || getEmptyComputedData()), + ...getEdgeSizes(data, { edgeSizeRatio: navState?.edgeSizeRatio }, dimensions), + })); + } + }, [data, navState?.edgeSizeRatio, dimensions]); + + /* eslint-disable react-hooks/exhaustive-deps */ + useEffect(() => { + if (!ready) return; + + let promise: + | Promise<{ + name: string; + extension: string; + textContent: string; + }> + | undefined; + + if (!url && !local) { + navigate("/?", { + state: { + error: MISSING_URL, + }, + }); + return; + } else if (local) { + if (localFile) { + promise = loadGraphFile(localFile); + } else { + navigate("/?", { + state: { + error: MISSING_FILE, + }, + }); + return; + } + } else { + promise = loadGraphURL(url as string); + } + + if (promise) { + promise + .then(({ name, extension, textContent }) => { + setGraphFile({ name, extension, textContent }); + return readGraph({ name, extension, textContent }); + }) + .then((rawGraph) => prepareGraph(rawGraph)) + .then(({ graph, report }) => { + const notif = getReportNotification(report, /*rawNavState.role !== "d"*/ true); + if (notif) notify(notif); + + const richData = enrichData(graph); + setData(richData); + + if (fromHome) { + setNavState({ + ...rawNavState, + ...guessNavState(richData, report), + }); + } + }) + .catch((e) => { + const error = e.name === BAD_URL ? BAD_URL : BAD_FILE; + console.error(getErrorMessage(error).replace(/\.$/, "") + ":"); + console.error(e.message); + navigate("/?", { + state: { + error: error, + }, + }); + }); + } else { + // This case should never occur, but TypeScript doesn't understand that; + navigate("/?", { + state: { + error: UNKNOWN, + }, + }); + } + }, [url, local, ready]); + /* eslint-enable react-hooks/exhaustive-deps */ + + if (!ready) + return ( +
setReady(true)} + > +

+ Retina logo +

+

Click here to see the graph visualization

+
+ ); + + if (!data || !graphFile || !navState || !computedData) return ; + + return ( + setModalName(modal), + closeModal: () => setModalName(undefined), + + panel, + setPanel, + + sigma, + setSigma, + root: domRoot.current || undefined, + }} + > + {navState.local && } + +
+
+ +
+ + + + +
+ +
+ +
+ +
+
+
+
+ + +
+ + {/* Currently opened modal: */} + {modalName && createElement(MODALS[modalName], { close: () => setModalName(undefined) })} +
+ ); +}; + +export default GraphView; diff --git a/src/views/HomeView.tsx b/src/views/HomeView.tsx new file mode 100644 index 0000000..460fa08 --- /dev/null +++ b/src/views/HomeView.tsx @@ -0,0 +1,151 @@ +import cx from "classnames"; +import React, { FC, useEffect, useState } from "react"; +import { AiOutlineCloud } from "react-icons/ai"; +import { RiComputerLine } from "react-icons/ri"; +import { useLocation, useNavigate } from "react-router"; + +import DropInput from "../components/DropInput"; +import Footer from "../components/Footer"; +import { SAMPLE_DATASET_URI } from "../lib/consts"; +import { getErrorMessage } from "../lib/errors"; +import { useNotifications } from "../lib/notifications"; + +const HomeView: FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const { notify } = useNotifications(); + const error = ((location.state as { error?: unknown } | undefined)?.error || "") + ""; + const [state, setState] = useState< + { type: "hidden" } | { type: "choice" } | { type: "url"; input: string } | { type: "local"; input: File | null } + >({ type: "hidden" }); + + useEffect(() => { + const id = setTimeout(() => setState({ type: "choice" }), 500); + return () => clearTimeout(id); + }, []); + + useEffect(() => { + if (error) + notify({ + message: getErrorMessage(error), + type: "error", + }); + }, [error, notify]); + + return ( +
+
+
+ DeepGit Logo +
+

+ + DeepGit{" "} + + beta + + +

+

+ DeepGit is a free, open-source web application designed to help researchers and research software engineers discover and explore research software within specific domains +

+

+ It currently accepts GEXF and{" "} + GraphML files. +

+
+
+ {state.type === "choice" && ( + <> +

Your graph file is...

+
+ + +
+ + )} + {state.type === "url" && ( +
{ + e.preventDefault(); + navigate(`/graph/?r=d&url=${encodeURIComponent(state.input)}`, { state: { fromHome: true } }); + }} + > + + setState({ type: "url", input: e.target.value })} + /> + + +
+ )} + {state.type === "local" && ( +
{ + e.preventDefault(); + navigate(`/graph/?r=d&l=1`, { state: { file: state.input as File, fromHome: true } }); + }} + > + setState({ type: "local", input: file })} /> + + + + )} +
+
+
+ +
+
+
+
+
+ ); +}; + +export default HomeView; diff --git a/src/views/LocalWarningBanner.tsx b/src/views/LocalWarningBanner.tsx new file mode 100644 index 0000000..8fdc9c7 --- /dev/null +++ b/src/views/LocalWarningBanner.tsx @@ -0,0 +1,46 @@ +import React, { FC, useContext } from "react"; +import { AiFillQuestionCircle } from "react-icons/ai"; + +import { GraphContext } from "../lib/context"; +import { queryURLToNavState } from "../lib/navState"; +import { useBlocker } from "../utils/useBlocker"; + +const LocalWarningBanner: FC = () => { + const { navState, openModal } = useContext(GraphContext); + + useBlocker((tx) => { + const newNavState = queryURLToNavState(tx.location.search); + + if ( + navState.preventBlocker || + !!navState.local === !!newNavState.local || + window.confirm( + "You are working on a local file, and you cannot share your visualizations yet. Are you sure you want to leave that page?", + ) + ) + tx.retry(); + }, navState.local); + + return ( + <> + {navState.local && ( +
+
+
+ You are currently using a local file, that only you can access. +
+
+ To be able to share your visualizations online, you need to first{" "} + publish your graph file online. +
+
+ +
+ )} + + ); +}; + +export default LocalWarningBanner; diff --git a/src/views/NodeSizeCaption.tsx b/src/views/NodeSizeCaption.tsx new file mode 100644 index 0000000..058a50e --- /dev/null +++ b/src/views/NodeSizeCaption.tsx @@ -0,0 +1,75 @@ +import { useSigma } from "@react-sigma/core"; +import { FC, useCallback, useContext, useEffect, useMemo, useState } from "react"; + +import { GraphContext } from "../lib/context"; +import { shortenNumber } from "../utils/number"; + +const NodeSizeCaption: FC = () => { + const { navState, data, computedData } = useContext(GraphContext); + + const { nodeSizeField } = navState; + const { fieldsIndex } = data; + const { getSize, nodeSizeExtents } = computedData; + const sigma = useSigma(); + + const sizeField = useMemo( + () => (nodeSizeField ? fieldsIndex[nodeSizeField] : undefined), + [fieldsIndex, nodeSizeField], + ); + const [state, setState] = useState<{ + minValue: number; + minRadius: number; + maxValue: number; + maxRadius: number; + } | null>(null); + + const refreshState = useCallback(() => { + if (!sigma || !sizeField || !nodeSizeExtents || !getSize) return null; + + const ratio = Math.sqrt(sigma.getCamera().ratio); + + setState({ + minValue: nodeSizeExtents[0], + minRadius: getSize(nodeSizeExtents[0]) / ratio, + maxValue: nodeSizeExtents[1], + maxRadius: getSize(nodeSizeExtents[1]) / ratio, + }); + }, [getSize, sigma, nodeSizeExtents, sizeField]); + + // Refresh caption when metric changes: + useEffect(() => { + refreshState(); + }, [nodeSizeExtents, sizeField, getSize, refreshState]); + + // Refresh caption on camera update: + useEffect(() => { + sigma.getCamera().addListener("updated", refreshState); + return () => { + sigma.getCamera().removeListener("updated", refreshState); + }; + }, [nodeSizeExtents, sizeField, getSize, sigma, refreshState]); + + if (!sizeField || !state) return null; + + return ( +
+

{sizeField.label}:

+
+
+
+
+
+
{shortenNumber(state.minValue)}
+
+
+
+
+
+
{shortenNumber(state.maxValue)}
+
+
+
+ ); +}; + +export default NodeSizeCaption; diff --git a/src/views/NodesAppearanceBlock.tsx b/src/views/NodesAppearanceBlock.tsx new file mode 100644 index 0000000..bf333b1 --- /dev/null +++ b/src/views/NodesAppearanceBlock.tsx @@ -0,0 +1,109 @@ +import cx from "classnames"; +import React, { FC, useContext, useMemo } from "react"; +import { BsPaletteFill } from "react-icons/bs"; +import { MdBubbleChart } from "react-icons/md"; +import Select from "react-select"; + +import { DEFAULT_SELECT_PROPS } from "../lib/consts"; +import { AppContext, GraphContext } from "../lib/context"; + +interface Option { + value: string; + label: string; + field?: string; +} + +const NodesAppearanceBlock: FC = () => { + const { portalTarget } = useContext(AppContext); + const { navState, data, setNavState } = useContext(GraphContext); + const { fieldsIndex } = data; + const { role, nodeColorField, nodeSizeField, colorable, sizeable, disableDefaultColor, disableDefaultSize } = + navState; + + const colorOptions: Option[] = useMemo( + () => [ + ...(disableDefaultColor ? [] : [{ value: "none", label: "Default (use colors from the graph file)" }]), + ...(colorable || []).map((key) => { + const field = fieldsIndex[key]; + return { + value: `${key}-field`, + label: field.label, + field: key, + }; + }), + ], + [colorable, fieldsIndex, disableDefaultColor], + ); + const colorOption = nodeColorField + ? colorOptions.find((o) => o.field === nodeColorField) || colorOptions[0] + : colorOptions[0]; + + const sizeOptions: Option[] = useMemo( + () => [ + ...(disableDefaultSize ? [] : [{ value: "none", label: "Default (use sizes from the graph file)" }]), + ...(sizeable || []).map((key) => { + const field = fieldsIndex[key]; + return { + value: `${key}-field`, + label: field.label, + field: key, + }; + }), + ], + [sizeable, fieldsIndex, disableDefaultSize], + ); + const sizeOption = nodeSizeField + ? sizeOptions.find((o) => o.field === nodeSizeField) || sizeOptions[0] + : sizeOptions[0]; + const showSizes = sizeOptions.length > 1; + const showColors = colorOptions.length > 1; + + if (!showSizes && !showColors) return null; + if (role === "v") return null; + + return ( +
+
+ {showColors && ( +
+ + setNavState({ ...navState, nodeSizeField: o?.field })} + isDisabled={sizeOptions.length <= 1} + /> +
+ )} +
+
+ ); +}; + +export default NodesAppearanceBlock; diff --git a/src/views/Notifications.tsx b/src/views/Notifications.tsx new file mode 100644 index 0000000..595c55b --- /dev/null +++ b/src/views/Notifications.tsx @@ -0,0 +1,97 @@ +import cx from "classnames"; +import { FC, useCallback, useState } from "react"; +import { CSSTransition } from "react-transition-group"; + +import { NotificationInput, useNotifications } from "../lib/notifications"; +import { useTimeout } from "../utils/useTimeout"; + +const CLASSES_TOAST = { + success: "bg-success", + info: "bg-info", + warning: "bg-warning", + error: "text-white bg-danger", +}; + +const CLASSES_TOAST_CLOSE: Record = { + error: "btn-close-white", +}; + +const CLASSES_ALERT = { + success: "alert-success", + info: "alert-info", + warning: "alert-warning", + error: "alert-danger", +}; + +const Notifications: FC = () => { + const { notifications, remove } = useNotifications(); + + return ( +
+ {notifications.map((notification) => ( + remove(notification.id)} + /> + ))} +
+ ); +}; + +export const Notification: FC<{ + notification: NotificationInput; + onClose?: () => void; + type?: "toast" | "text"; +}> = ({ notification, onClose, type }) => { + const [show, setShow] = useState(true); + + const setShowFalse = useCallback(() => setShow(false), [setShow]); + const { cancel, reschedule } = useTimeout( + setShowFalse, + notification.keepAlive ? -1 : notification.type !== "success" ? 10000 : 5000, + ); + + if (type === "toast") { + const notifType = notification.type || "info"; + return ( + { + if (onClose) onClose(); + }} + > +
+
+
{notification.message}
+ {onClose && ( +
+
+
+ ); + } + + return ( +
+ {notification.message} + {onClose &&
+ ); +}; + +export default Notifications; diff --git a/src/views/ReadabilityBlock.tsx b/src/views/ReadabilityBlock.tsx new file mode 100644 index 0000000..7a85ada --- /dev/null +++ b/src/views/ReadabilityBlock.tsx @@ -0,0 +1,241 @@ +import cx from "classnames"; +import { isEqual } from "lodash"; +import Slider, { SliderProps } from "rc-slider"; +import React, { FC, useContext, useState } from "react"; +import { FaTimes, FaUndo } from "react-icons/fa"; +import { FaGear } from "react-icons/fa6"; +import { VscSettings } from "react-icons/vsc"; + +import { + DEFAULT_EDGE_SIZE_RATIO, + DEFAULT_LABEL_SIZE, + DEFAULT_LABEL_THRESHOLD, + DEFAULT_NODE_SIZE_RATIO, + LABEL_SIZE_STEP, + LABEL_THRESHOLD_STEP, + MAX_LABEL_SIZE, + MAX_LABEL_THRESHOLD, + MAX_NODE_SIZE_RATIO, + MIN_LABEL_SIZE, + MIN_LABEL_THRESHOLD, + MIN_NODE_SIZE_RATIO, + NODE_SIZE_RATIO_STEP, + RANGE_STYLE, + SLIDER_STYLE, +} from "../lib/consts"; +import { GraphContext } from "../lib/context"; +import { NavState } from "../lib/navState"; + +const ReadabilityBlock: FC = () => { + const { navState, setNavState } = useContext(GraphContext); + + const [initialNavState] = useState(navState); + + const minLabelSize = typeof navState.minLabelSize === "number" ? navState.minLabelSize : DEFAULT_LABEL_SIZE; + const maxLabelSize = typeof navState.maxLabelSize === "number" ? navState.maxLabelSize : DEFAULT_LABEL_SIZE; + const nodeSizeRatio = typeof navState.nodeSizeRatio === "number" ? navState.nodeSizeRatio : DEFAULT_NODE_SIZE_RATIO; + const edgeSizeRatio = typeof navState.edgeSizeRatio === "number" ? navState.edgeSizeRatio : DEFAULT_EDGE_SIZE_RATIO; + const labelThresholdRatio = + typeof navState.labelThresholdRatio === "number" ? navState.labelThresholdRatio : DEFAULT_LABEL_THRESHOLD; + + const cancel = () => setNavState(initialNavState); + + return ( +
+

+ Settings +

+ +
+ +
+ + + {navState.role !== "v" && ( + + )} +
+ +
+ +
+

+ + +

+
+ { + setNavState({ ...navState, minLabelSize, maxLabelSize }); + }) as SliderProps["onChange"] + } + // Styles: + {...RANGE_STYLE} + /> +
+
+ +
+

+ + +

+
+ { + setNavState({ ...navState, nodeSizeRatio: v }); + }) as SliderProps["onChange"] + } + // Styles: + {...SLIDER_STYLE} + /> +
+
+ +
+

+ + +

+
+ { + setNavState({ ...navState, edgeSizeRatio: v }); + }) as SliderProps["onChange"] + } + // Styles: + {...SLIDER_STYLE} + /> +
+
+ +
+

+ + +

+
+ { + setNavState({ ...navState, labelThresholdRatio: v }); + }) as SliderProps["onChange"] + } + // Styles: + {...SLIDER_STYLE} + /> +
+
+
+ ); +}; + +export default ReadabilityBlock; diff --git a/src/views/Root.tsx b/src/views/Root.tsx new file mode 100644 index 0000000..94354cc --- /dev/null +++ b/src/views/Root.tsx @@ -0,0 +1,25 @@ +import React, { FC } from "react"; +import { Route } from "react-router"; +import { HashRouter, Navigate, Routes } from "react-router-dom"; + +import GraphView from "./GraphView"; +import HomeView from "./HomeView"; +import Notifications from "./Notifications"; + +const Root: FC = () => { + return ( + <> + + + } /> + } /> + } /> + } /> + + + + + ); +}; + +export default Root; diff --git a/src/views/SelectedNodePanel.tsx b/src/views/SelectedNodePanel.tsx new file mode 100644 index 0000000..8c593e7 --- /dev/null +++ b/src/views/SelectedNodePanel.tsx @@ -0,0 +1,134 @@ +import cx from "classnames"; +import { map, mapKeys, omitBy, startCase, uniq } from "lodash"; +import React, { FC, useContext } from "react"; +import { BiRadioCircleMarked } from "react-icons/bi"; +import { FaTimes } from "react-icons/fa"; +import Linkify from "react-linkify"; +import { Coordinates } from "sigma/types"; + +import Connection from "../components/Connection"; +import Node from "../components/Node"; +import { ANIMATION_DURATION, DEFAULT_LINKIFY_PROPS, isHiddenRetinaField, removeRetinaPrefix } from "../lib/consts"; +import { GraphContext } from "../lib/context"; +import { NodeData } from "../lib/data"; + +const HIDDEN_KEYS = new Set(["x", "y", "z", "size", "label", "color"]); + +const SelectedNodePanel: FC<{ node: string; data: NodeData }> = ({ node, data: { attributes } }) => { + const { + navState, + setNavState, + data: { graph }, + sigma, + computedData: { filteredNodes }, + } = useContext(GraphContext); + + if (!attributes) return null; + + const currentAttributes = graph.getNodeAttributes(node); + const filteredAttributes = mapKeys( + omitBy(attributes, (_, key) => isHiddenRetinaField(key) || HIDDEN_KEYS.has(key)), + (_, key) => removeRetinaPrefix(key), + ); + const visibleNeighbors: string[] = []; + const hiddenNeighbors: string[] = []; + uniq(graph.neighbors(node)).forEach((n) => { + if (filteredNodes && !filteredNodes.has(n)) hiddenNeighbors.push(n); + else visibleNeighbors.push(n); + }); + + const isHidden = filteredNodes && !filteredNodes.has(node); + + return ( +
+

+ + {currentAttributes.label} + {isHidden ? ( + <> + {" "} + (currently filtered out) + + ) : null} +

+ +
+ +
+ + +
+ +
+ + {map(filteredAttributes, (value, key) => ( +

+ {startCase(key)}:{" "} + + {typeof value === "number" ? value.toLocaleString() : {value}} + +

+ ))} + +
+ + {!(visibleNeighbors.length + hiddenNeighbors.length) &&

This node has no neighbor.

} + + {!!visibleNeighbors.length && ( + <> +
+ This node has {visibleNeighbors.length > 1 ? visibleNeighbors.length + " neighbors" : "one neighbor"}{" "} + visible in this graph: +
+
    + {visibleNeighbors.map((neighbor) => ( +
  • + + +
  • + ))} +
+ + )} + + {!!hiddenNeighbors.length && ( + <> +
+ This node{visibleNeighbors.length ? " also" : ""} has{" "} + {hiddenNeighbors.length > 1 ? hiddenNeighbors.length + " neighbors " : "one neighbor "} + that {hiddenNeighbors.length > 1 ? "are" : "is"} currently filtered out: +
+
    + {hiddenNeighbors.map((neighbor) => ( +
  • + + +
  • + ))} +
+ + )} +
+ ); +}; + +export default SelectedNodePanel; diff --git a/src/views/modals/PublishModal.tsx b/src/views/modals/PublishModal.tsx new file mode 100644 index 0000000..c25bc83 --- /dev/null +++ b/src/views/modals/PublishModal.tsx @@ -0,0 +1,207 @@ +import { noop } from "lodash"; +import React, { FC, useContext, useState } from "react"; +import { AiOutlineCheckCircle, AiOutlineCloudUpload } from "react-icons/ai"; +import { FiCopy } from "react-icons/fi"; + +import Modal from "../../components/Modal"; +import { GraphContext } from "../../lib/context"; +import { useNotifications } from "../../lib/notifications"; + +const PublishModal: FC<{ close: () => void }> = ({ close }) => { + const { notify } = useNotifications(); + const { graphFile, navState, setNavState } = useContext(GraphContext); + const [url, setUrl] = useState(""); + const [state, setState] = useState<{ type: "idle"; errorMessage?: string } | { type: "loading" }>({ + type: "idle", + }); + + const handleSubmit = async () => { + if (state.type !== "idle") return; + + setState({ type: "loading" }); + fetch(url) + .then((response) => response.text()) + .then((textData) => { + if (textData === graphFile.textContent) { + // First, update navState so that blocker is skipped next update: + setNavState({ + ...navState, + preventBlocker: true, + }); + + // Then, actually update the navState: + setNavState({ + ...navState, + url, + local: undefined, + }); + notify({ + type: "success", + message: "Congrats, you can now share your graph online!", + }); + close(); + } else { + setState({ + type: "idle", + errorMessage: "The file at the given URL does not match the one you are using now.", + }); + } + }) + .catch(() => { + setState({ + type: "idle", + errorMessage: "The file at the given URL could not be properly loaded.", + }); + }); + }; + + return ( + + Publish your graph online + + } + onClose={state.type !== "loading" ? close : noop} + > +
{ + e.preventDefault(); + handleSubmit(); + }} + > +

+ To be able to share your visualizations online, Retina needs to be able to access your graph + file online, through HTTP. You can publish it on a server or your own, a cloud provider... +

+ +

+ Here is how to upload it on{" "} + + GitHub Gist + + , a site where you can freely upload your graph for Retina: +

+ +
    +
  1. + Go to{" "} + + gist.github.com + + , create an account (if not done already) and log in +
  2. +
  3. + + Click{" "} + {" "} + to copy your graph file content + +
  4. +
  5. + Create{" "} + + a new gist + +
  6. +
  7. Paste your file content in the main input (the big white rectangle)
  8. +
  9. + + Click{" "} + {" "} + to copy your graph file name + +
  10. +
  11. + Paste your file name in the Filename including extension… input +
  12. +
  13. + Click on Create secret gist +
  14. +
  15. + Click on the Raw button (on the top right of the graph file content) +
  16. +
+

+ At this point, you should have a webpage with only the graph content visible. That means your graph has + properly been uploaded! +

+

+ +

+
+ setUrl(e.target.value)} + /> +
+ {state.type === "idle" && !!state.errorMessage &&
{state.errorMessage}
} +
+ + +
+
+
+ ); +}; + +export default PublishModal; diff --git a/src/views/modals/ShareModal.tsx b/src/views/modals/ShareModal.tsx new file mode 100644 index 0000000..0486da2 --- /dev/null +++ b/src/views/modals/ShareModal.tsx @@ -0,0 +1,164 @@ +import React, { ChangeEvent, FC, useContext, useMemo, useRef, useState } from "react"; +import { BsShare } from "react-icons/bs"; +import { FiCopy } from "react-icons/fi"; + +import Modal from "../../components/Modal"; +import { GraphContext } from "../../lib/context"; +import { Data } from "../../lib/data"; +import { cleanNavState, navStateToQueryURL } from "../../lib/navState"; +import { useNotifications } from "../../lib/notifications"; + +const ShareModal: FC<{ close: () => void }> = ({ close }) => { + const { notify } = useNotifications(); + const { data, navState } = useContext(GraphContext); + + const [shareMode, setShareMode] = useState<"x" | "v">("x"); + const [isEmbed, setIsEmbed] = useState(false); + const onEmbedChange = (e: ChangeEvent) => setIsEmbed(e.target.value === "embed"); + const codeOrURLLabel = isEmbed ? "code" : "URL"; + + const { origin, pathname } = window.location; + const shareURL = useMemo(() => { + return ( + origin + + pathname + + `#/${isEmbed ? "embed" : "graph"}/?` + + navStateToQueryURL(cleanNavState({ ...navState, role: shareMode }, data as Data)) + ); + }, [data, isEmbed, navState, origin, pathname, shareMode]); + + const domCode = useRef(null); + const copyCodeOrURL = () => { + if (isEmbed && !domCode.current) { + notify({ + type: "error", + message: `An error occurred while trying to copy the ${codeOrURLLabel}.`, + }); + } + + const codeOrURL = isEmbed ? domCode.current!.innerText : shareURL; + + navigator.clipboard + .writeText(codeOrURL) + .then(() => + notify({ + type: "success", + message: `The ${codeOrURLLabel} is copied to your clipboard.`, + }), + ) + .catch(() => + notify({ + type: "error", + message: `An error occurred while trying to copy the ${codeOrURLLabel}.`, + }), + ); + }; + + return ( + + Share this graph + + } + onClose={close} + > + <> +
+ +
+
+ + +
+
+ + +
+
+
+ +
+ +
+ setShareMode(e.target.checked ? "x" : "v")} + /> + +
+ +
+ +

+ {isEmbed ? ( + <>You can embed this graph in your website using this code: + ) : ( + <>You can use this URL to share the graph with other users: + )} +

+ {isEmbed ? ( +
+            {``}
+          
+ ) : ( +
+ + + {shareURL} + +
+ )} + +
+ {isEmbed && ( + + )} + +
+
+ ); +}; + +export default ShareModal; diff --git a/src/views/modals/index.ts b/src/views/modals/index.ts new file mode 100644 index 0000000..bca9dc9 --- /dev/null +++ b/src/views/modals/index.ts @@ -0,0 +1,9 @@ +import PublishModal from "./PublishModal"; +import ShareModal from "./ShareModal"; + +export const MODALS = { + publish: PublishModal, + share: ShareModal, +} as const; + +export type ModalName = keyof typeof MODALS; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bb4c182 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "downlevelIteration": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/vite-env.d.ts b/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/vite.config.mts b/vite.config.mts new file mode 100644 index 0000000..280a97e --- /dev/null +++ b/vite.config.mts @@ -0,0 +1,9 @@ +import react from "@vitejs/plugin-react-swc"; +import { defineConfig } from "vite"; + +// https://vitejs.dev/config/ +console.log(`Building DeepGit with BASE_PATH="${process.env.BASE_PATH || "/deepgit"}"`); +export default defineConfig({ + base: process.env.BASE_PATH || "/deepgit", + plugins: [react()], +}); diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 0000000..44d4a49 --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,13 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + browser: { + provider: "playwright", + name: "chromium", + enabled: true, + headless: true, + }, + }, +});