diff --git a/nx.json b/nx.json index 2f382d35c7..9af871103b 100644 --- a/nx.json +++ b/nx.json @@ -279,5 +279,6 @@ "defaultBase": "main", "tui": { "enabled": false - } + }, + "analytics": false } diff --git a/packages/twenty-ui/.size-limit.json b/packages/twenty-ui/.size-limit.json index 982175df27..7c52e5c70b 100644 --- a/packages/twenty-ui/.size-limit.json +++ b/packages/twenty-ui/.size-limit.json @@ -18,42 +18,42 @@ { "name": "testing", "path": "dist/testing.mjs", - "limit": "10 kB" + "limit": "188 kB" }, { "name": "components", "path": "dist/components.mjs", - "limit": "40 kB" + "limit": "209 kB" }, { "name": "display", "path": "dist/display.mjs", - "limit": "30 kB" + "limit": "509 kB" }, { "name": "feedback", "path": "dist/feedback.mjs", - "limit": "20 kB" + "limit": "164 kB" }, { "name": "input", "path": "dist/input.mjs", - "limit": "50 kB" + "limit": "278 kB" }, { "name": "layout", "path": "dist/layout.mjs", - "limit": "20 kB" + "limit": "230 kB" }, { "name": "navigation", "path": "dist/navigation.mjs", - "limit": "20 kB" + "limit": "219 kB" }, { "name": "json-visualizer", "path": "dist/json-visualizer.mjs", - "limit": "30 kB" + "limit": "173 kB" }, { "name": "theme", @@ -68,7 +68,7 @@ { "name": "utilities", "path": "dist/utilities.mjs", - "limit": "10 kB" + "limit": "217 kB" }, { "name": "style.css", diff --git a/packages/twenty-ui/.storybook/preview.tsx b/packages/twenty-ui/.storybook/preview.tsx index 05f01e5737..f129471e5b 100644 --- a/packages/twenty-ui/.storybook/preview.tsx +++ b/packages/twenty-ui/.storybook/preview.tsx @@ -1,7 +1,7 @@ import { type Preview } from '@storybook/react-vite'; -import '@new-ui/theme-constants/theme-light.css'; -import '@new-ui/theme-constants/theme-dark.css'; -import { ThemeProvider } from '@new-ui/theme-constants'; +import '@ui/theme-constants/theme-light.css'; +import '@ui/theme-constants/theme-dark.css'; +import { ThemeProvider } from '@ui/theme-constants'; const preview: Preview = { tags: ['autodocs'], diff --git a/packages/twenty-ui/.storybook/vitest.setup.ts b/packages/twenty-ui/.storybook/vitest.setup.ts index 512a7f55d2..26d684b986 100644 --- a/packages/twenty-ui/.storybook/vitest.setup.ts +++ b/packages/twenty-ui/.storybook/vitest.setup.ts @@ -1,3 +1,4 @@ +import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview'; import { setProjectAnnotations } from '@storybook/react-vite'; import { MotionGlobalConfig } from 'framer-motion'; import * as projectAnnotations from './preview'; @@ -9,4 +10,6 @@ import * as projectAnnotations from './preview'; MotionGlobalConfig.skipAnimations = true; // Apply Storybook's preview configuration to Vitest runs. -setProjectAnnotations([projectAnnotations]); +// The a11y addon annotations register the axe afterEach hook so that +// parameters.a11y.test = 'error' (set in preview.tsx) actually gates tests. +setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]); diff --git a/packages/twenty-ui/LICENSE b/packages/twenty-ui/LICENSE new file mode 100644 index 0000000000..50a6a10a28 --- /dev/null +++ b/packages/twenty-ui/LICENSE @@ -0,0 +1,710 @@ + +This project is mostly licensed under the GNU General Public License (GPL) as described below. However, certain files within this project are licensed under a different commercial license. These files are clearly marked with the following comment at the top of the file: /* @license Enterprise */ +Files with this comment are not licensed under the aGPL v3, but instead are subject to the commercial license terms defined later in this file. + + + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. + + + +------------------------------------ + + + + The Twenty.com Commercial License (the “Commercial License”) + Copyright (c) 2023-present Twenty.com, PBC + +With regard to Twenty's Software: + +This part of the software and associated documentation files (the "Software") may only be +used in production, if you (and any entity that you represent) have agreed to, +and are in compliance with, the Terms available +at https://twenty.com/legal/terms, or other agreements governing +the use of the Software, as mutually agreed by you and Twenty.com, PBC ("Twenty"), +and otherwise have a valid Twenty Enterprise Edition subscription +for the correct number of hosts and seats as defined in the Commercial Terms. +Subject to the foregoing sentence, +you are free to modify this Software and publish patches to the Software. You agree +that Twenty and/or its licensors (as applicable) retain all right, title and interest in +and to all such modifications and/or patches, and all such modifications and/or +patches may only be used, copied, modified, displayed, distributed, or otherwise +exploited with a valid Commercial Subscription for the correct number of hosts and seats. +Notwithstanding the foregoing, you may copy and modify the Software for development +and testing purposes, without requiring a subscription. You agree that Twenty.Com and/or +its licensors (as applicable) retain all right, title and interest in and to all such +modifications. You are not granted any other rights beyond what is expressly stated herein. +Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, +and/or sell the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +For all third party components incorporated into the Twenty Software, those +components are licensed under the original license provided by the owner of the +applicable component. \ No newline at end of file diff --git a/packages/twenty-ui/README.md b/packages/twenty-ui/README.md index fa425063b0..ca43972d15 100644 --- a/packages/twenty-ui/README.md +++ b/packages/twenty-ui/README.md @@ -1,23 +1,28 @@ # twenty-ui -> **Status:** Phase 0 — Foundations in progress. The package is scaffolded and builds -> on SCSS Modules + Base UI; the theme layer is ported from `twenty-ui` with a parity -> test, and the size/Storybook/a11y harnesses are wired up. Remaining Phase 0 work: the -> CI diff-table workflow, the `twenty-ui` component inventory, and the `modules/ui` triage. -> The sections below remain the design document for the full effort. +> **Status:** Phases 0–3 complete (June 2026). All 192 components and all 70 stories were +> migrated from `twenty-ui-deprecated` with full public-API parity (export-identifier diff: +> 0 missing / 0 extra across all 13 subpath modules) and byte-identical story titles for the +> Argos cross-package diff. Gates green: typecheck, lint, jest, build, storybook:build, +> storybook:test (225 stories incl. play functions + live axe gate), size. +> Remaining: the Argos visual-parity triage, the a11y fix pass (119 stories carry +> inherited-violation `a11y: 'todo'` overrides), the CI diff-table workflow, the +> `modules/ui` triage (Phase 4), and the publish pipeline (Phase 5). +> The sections below remain the design document for the full effort; see +> [Migration outcomes](#migration-outcomes-june-2026) for decisions taken during the port. -`twenty-ui` is the next generation of Twenty's UI library, replacing [`twenty-ui`](../twenty-ui). +`twenty-ui` is the next generation of Twenty's UI library, replacing [`twenty-ui-deprecated`](../twenty-ui-deprecated). It is built on a headless component library and a zero-runtime, CSS-variable styling layer. ## Goals 1. Publish as a standalone, versioned **npm package**. 2. Replace `twenty-ui` in `twenty-front` with **no visual change** (same design, token for token). -3. **Migrate every component** currently exported by `twenty-ui`. +3. **Migrate every component** currently exported by `twenty-ui-deprecated`. *(Done — June 2026.)* 4. **Absorb the generic, reusable UI** currently living in `twenty-front/src/modules/ui` (dropdowns, modals, tab lists, side panels, navigation, field inputs/displays, etc.), decoupling it from application concerns so it ships from the library. 5. Enforce a **quality bar in CI**: bundle size, render/load time, and accessibility, measured against the old library. -## Current state (`twenty-ui`) +## Current state (`twenty-ui-deprecated`) | Aspect | Today | | --- | --- | @@ -26,10 +31,10 @@ It is built on a headless component library and a zero-runtime, CSS-variable sty | Behavior | Hand-rolled (modals, menus, tooltips, selects, etc.); `react-tooltip` for tooltips | | Build | Vite library mode, dual ESM/CJS, `vite-plugin-dts`, auto-generated barrels | | Icons | `@tabler/icons-react` re-exports + custom icons + Jotai-backed `IconsProvider` | -| Consumption | ~1,721 files in `twenty-front` import it (mostly `display` and `theme-constants`); imported by package name | +| Consumption | ~1,730 files in `twenty-front` import it (mostly `display` and `theme-constants`), plus `twenty-front-component-renderer` and `twenty-sdk`; imported by package name | | Published | No (`private: true`) | -`twenty-front/src/modules/ui/` (application-level UI) consumes `twenty-ui` today. Its **generic, reusable** +`twenty-front/src/modules/ui/` (application-level UI) consumes `twenty-ui-deprecated` today. Its **generic, reusable** components are now **in scope** — they migrate into `twenty-ui` (see [Application-level UI migration](#application-level-ui-migration-twenty-frontsrcmodulesui)). ## Decision 1 — Headless library: Base UI @@ -98,30 +103,32 @@ packages/twenty-ui/ ``` **Public API parity.** Keep the same subpath exports, component names, and prop signatures as -`twenty-ui` so the final swap is a codemod + dependency rename, not a rewrite of 1,721 files. -Keep auto-generated barrels and dual ESM/CJS + `dts` output. +`twenty-ui-deprecated` so the final swap is a codemod + dependency rename, not a rewrite of ~1,730 files. +Keep auto-generated barrels and dual ESM/CJS + `dts` output. *(Achieved: the export-identifier sets of +all 13 module barrels are identical between the two packages.)* -**Internal changes vs `twenty-ui`:** Linaria → SCSS Modules; hand-rolled behavior + `react-tooltip` +**Internal changes vs `twenty-ui-deprecated`:** Linaria → SCSS Modules; hand-rolled behavior + `react-tooltip` → Base UI; prefer Base UI/CSS transitions over `framer-motion` where possible; keep the icon system -as-is. +as-is. The internal path alias is `@ui/*` (same convention as the deprecated package, so ported files +diff cleanly against their sources). ## Theming `theme-constants` has ~943 importers and must be a drop-in replacement. - Keep the public API identical: `ThemeProvider`, `ThemeContext`, `useTheme`, the `themeCssVariables` shape, `ThemeType`, color helpers, and the `theme-light.css` / `theme-dark.css` exports. -- Reuse `twenty-ui`'s token values verbatim to guarantee identical design. -- Tokens live in `src/theme/` (`THEME_LIGHT` / `THEME_DARK`); the `--t-*` CSS variables and the `themeCssVariables` accessor are static files mirrored token-for-token from `twenty-ui` (matching `twenty-ui`'s own static-CSS approach). -- A theme parity test asserts the theme CSS and `themeCssVariables` stay identical to `twenty-ui`'s `--t-*` values. +- Reuse `twenty-ui-deprecated`'s token values verbatim to guarantee identical design. +- Tokens live in `src/theme/` (`THEME_LIGHT` / `THEME_DARK`); the `--t-*` CSS variables and the `themeCssVariables` accessor are static files mirrored token-for-token from `twenty-ui-deprecated` (matching its own static-CSS approach). +- A theme parity test asserts the theme CSS and `themeCssVariables` stay identical to `twenty-ui-deprecated`'s `--t-*` values. ## Component migration map -A full component-by-component inventory with prop signatures is a Phase 0 deliverable. Components -split into two buckets. +Components split into two buckets. *(The migration is complete; see +[Migration outcomes](#migration-outcomes-june-2026) below for where reality diverged from this map.)* **Backed by a Base UI primitive (behavioral):** -| `twenty-ui` | Base UI | +| `twenty-ui-deprecated` | Base UI | | --- | --- | | `Modal` | `Dialog` / `AlertDialog` | | `AppTooltip`, `OverflowingTextWithTooltip` | `Tooltip` (removes `react-tooltip`) | @@ -141,15 +148,43 @@ split into two buckets. `ColorSample`, `Checkmark`, placeholders, the icon system, `CodeEditor` (Monaco), `json-visualizer`, and the `utilities` / `theme` / `testing` / `accessibility` helpers. +### Migration outcomes (June 2026) + +Decisions taken during the port where reality diverged from the map above (public API parity won every time): + +- **`MenuItem` family and `TabButton` did NOT move to Base UI** Menu/Tabs — those primitives require a + `Root` context the deprecated standalone APIs don't have. They are pure presentation with `data-*` state. + The Base UI `Menu` mapping applies to the Phase-4 `modules/ui` dropdown instead. +- **`AnimatedExpandableContainer` kept framer-motion** (sanctioned fallback): its exported utils are framer + variant factories whose signatures are public API, and Collapsible/grid-rows could not match scrollHeight + mid-animation retargeting or initial-mount animation. +- **`Avatar`'s `invalidAvatarUrlsAtomV2` and the icon system's `iconsState` Jotai atoms were kept** — both are + public `display` exports, overriding this doc's earlier "Jotai → local state" idea. +- **`CardContent` kept framer-motion**: its motion props are part of the public prop type and twenty-front's + `CalendarDayCardContent` passes them. +- **`ProgressBar` is pure SCSS** (Base UI Progress was optional and not needed for parity); `useProgressAnimation` + keeps framer motion-values verbatim (public hook). +- **Known behavior deltas to watch in QA:** `AppTooltip` and `Modal` (without an explicit `container`) now portal + to `document.body` (Base UI), so overflow-clipping ancestors no longer clip them and `AppTooltip`'s default + `maxWidth: '40%'` resolves against the body; `Checkbox`/`Radio` test ids moved from the hidden input to the + visible Base UI control. +- **The Storybook axe gate was inert until this migration** — `.storybook/vitest.setup.ts` did not register the + a11y addon annotations, so `a11y.test: 'error'` never ran. It is now live; 119 ported stories carry + story-level `a11y: { test: 'todo' }` overrides (`// TODO(a11y)` comments) pending a dedicated fix pass. +- The canonical styling pattern is established in `src/input/button/components/Button/Button.tsx` + + `Button.module.scss`: `data-variant/accent/position/inverted` attributes, Sass maps + `@each` assigning + `--btn-*` custom properties, all declarations on a flat `(0,1,0)` class so `styled(Component)` consumer + overrides keep working. + ## Application-level UI migration (`twenty-front/src/modules/ui`) -`twenty-front/src/modules/ui` holds ~250 application-level UI building blocks that consume `twenty-ui` +`twenty-front/src/modules/ui` holds ~250 application-level UI building blocks that consume `twenty-ui-deprecated` today: `display`, `feedback` (snackbar/dialog managers), `field` (input + display), `input` (incl. relation picker), `layout` (dropdown, modal, tab-list, side-panel, page, table, resizable-panel, expandable-list, selectable-list, top-bar, …), `navigation` (drawer, breadcrumb, step-bar, menu-item), `drag-and-drop`, `suggestion`, `theme`, and `utilities` (hotkey, scroll, focus, responsive, drag-select, …). -These are a **different kind of migration** than the `twenty-ui` swap: they are stateful and +These are a **different kind of migration** than the `twenty-ui-deprecated` swap: they are stateful and app-coupled — Jotai atoms, hooks, contexts, and (in places) GraphQL/router/Recoil-style state — rather than pure presentation. The goal is to extract the **generic, reusable** parts into `twenty-ui` while leaving genuinely app-specific wiring in `twenty-front`. @@ -165,14 +200,14 @@ while leaving genuinely app-specific wiring in `twenty-front`. workspace/router/permission logic, and anything whose only consumer is a single feature screen. A component-by-component triage of `modules/ui` (generic / app-specific / hybrid, with target subpath -and state-decoupling notes) is a **Phase 0 deliverable**, alongside the `twenty-ui` inventory. +and state-decoupling notes) is the remaining **Phase 4 prerequisite** (the `twenty-ui-deprecated` inventory itself is complete — every component is migrated). ## Hardest components to migrate (risk hotspots) A predicted ranking of where the effort and risk concentrate, to inform sequencing and staffing. This is a hypothesis to validate during the Phase 0 inventory/triage, not a final list. -### In `twenty-ui` +### In `twenty-ui-deprecated` *(all migrated — predictions held; see Migration outcomes)* | Component | Why it's hard | Migration shape | | --- | --- | --- | @@ -215,23 +250,23 @@ Apollo error formatting, and the icon/theme-color pickers tied to Twenty's icon - **Workbench** — Storybook (`@storybook/react-vite`). Every component has stories covering variants, sizes, and states (via `storybook-addon-pseudo-states`), in light and dark, with `autodocs`. - **Functional** — component/interaction tests via `@storybook/addon-vitest` (real browser); unit tests (Jest) for hooks/utilities; coverage gate via `@storybook/addon-coverage`. - **Accessibility** — Storybook a11y addon (axe-core) with `parameters.a11y.test = 'error'` so violations fail CI. -- **Visual parity** — visual regression via Argos (self-hosted) plus a cross-package comparison project that diffs `twenty-ui` stories against `twenty-ui` stories with identical names; a pixel-diff threshold is the per-component acceptance gate. See [Visual regression](#visual-regression) below. -- **Performance & size** — `size-limit` per entry point with budgets; tree-shaking fixtures (importing one component must not pull the library); build-time tracking; render benchmarks via React Profiler; load-time via Lighthouse/Playwright on the built Storybook. As one concrete benchmark, a dedicated **stress story** renders a very large number of a single component (e.g. 10,000 buttons) and measures total render time — compared against the `twenty-ui` equivalent and gated against a budget to catch per-instance overhead regressions. +- **Visual parity** — visual regression via Argos (self-hosted) plus a cross-package comparison project that diffs `twenty-ui` stories against `twenty-ui-deprecated` stories with identical names; a pixel-diff threshold is the per-component acceptance gate. See [Visual regression](#visual-regression) below. +- **Performance & size** — `size-limit` per entry point with budgets; tree-shaking fixtures (importing one component must not pull the library); build-time tracking; render benchmarks via React Profiler; load-time via Lighthouse/Playwright on the built Storybook. As one concrete benchmark, a dedicated **stress story** renders a very large number of a single component (e.g. 10,000 buttons) and measures total render time — compared against the `twenty-ui-deprecated` equivalent and gated against a budget to catch per-instance overhead regressions. *(Stress story not yet built.)* -CI surfaces a per-PR diff table (`twenty-ui` vs `twenty-ui`) for size, a11y, and visual changes. +CI surfaces a per-PR diff table (`twenty-ui-deprecated` vs `twenty-ui`) for size, a11y, and visual changes. *(Not yet built.)* ## Visual regression Two Argos projects (on argos.twenty-internal.com) provide visual regression in CI: 1. **`twenty-ui`** — pixel diff of `twenty-ui` stories against the `main` branch baseline. Catches regressions introduced by a PR. -2. **`twenty-ui-vs-new-ui`** — cross-package comparison. The baseline is always `twenty-ui` screenshots from `main`; PR builds upload `twenty-ui` screenshots and diff them against the `twenty-ui` baseline. This shows exactly which components still differ between the two implementations. +2. **`twenty-ui-vs-new-ui`** — cross-package comparison. The baseline is always `twenty-ui-deprecated` screenshots from `main`; PR builds upload `twenty-ui` screenshots and diff them against that baseline. This shows exactly which components still differ between the two implementations. -For the cross-package comparison to produce meaningful diffs, stories in `twenty-ui` must use the **same title hierarchy** as `twenty-ui` (e.g. `UI/Input/Toggle`). +For the cross-package comparison to produce meaningful diffs, stories in `twenty-ui` must use the **same title hierarchy** as `twenty-ui-deprecated` (e.g. `UI/Input/Toggle/Toggle`). *(Done: all 70 story titles are byte-identical.)* ### Local visual diff -Run a pixel diff of `twenty-ui` components against `twenty-ui` using the self-hosted Argos instance. +Run a pixel diff of `twenty-ui` components against `twenty-ui-deprecated` using the self-hosted Argos instance. **Prerequisites:** - AWS SSO configured and logged in (`aws sso login --profile twenty-dev`) @@ -262,10 +297,10 @@ This builds Storybook, captures screenshots of every story, and uploads them to Argos with build name `/twenty-ui`. The diff compares against the latest approved baseline. -To run `twenty-ui`'s visual diff in the same Argos instance (to build the +To run `twenty-ui-deprecated`'s visual diff in the same Argos instance (to build the cross-package comparison baseline): - npx nx storybook:visual-diff twenty-ui + npx nx storybook:visual-diff twenty-ui-deprecated **4. View results** @@ -276,28 +311,28 @@ to review diffs. - Vite library mode, dual ESM/CJS, `vite-plugin-dts`, `vite-plugin-svgr`; SCSS via Vite's built-in `sass`; no Babel. - `sideEffects: ["**/*.css", "**/*.scss"]`; emit per-entry CSS plus `style.css` / `theme-light.css` / `theme-dark.css`. -- Public package (remove `private`); ships as `twenty-ui` until cut-over, then claims the `twenty-ui` name once the old package is removed. +- Public package (remove `private`); ships as `twenty-ui` (the old package already moved to the `twenty-ui-deprecated` name and is removed after cut-over). - **Changesets** for semver + changelog; GitHub Actions release with `npm publish --provenance`. - Declare `react` / `react-dom` as peer dependencies; validate the `exports`/types map with `publint` + `@arethetypeswrong/cli`. - Publish the Storybook as living documentation. ## Migration & rollout -1. Build `twenty-ui` to parity with the same API surface and design, validated by the parity, a11y, and size suites. +1. ~~Build `twenty-ui` to parity with the same API surface and design~~ **Done** — validated by the export-parity diff, a11y, and size suites; visual-parity triage via Argos still pending. 2. Dogfood on a few non-critical `twenty-front` screens behind a temporary alias. -3. Codemod imports `twenty-ui` → `twenty-ui` across `twenty-front` (subpaths preserved); handle any changed APIs explicitly. +3. Codemod imports `twenty-ui-deprecated` → `twenty-ui` (subpaths preserved) across `twenty-front` (~1,730 files), `twenty-front-component-renderer`, and `twenty-sdk`; handle any changed APIs explicitly. 4. Swap the dependency, run the full test suite + visual diffs, ship. -5. Deprecate and remove `twenty-ui` after a soak period. +5. Remove `twenty-ui-deprecated` after a soak period. ## Roadmap -- **Phase 0 — Foundations:** scaffold package + tooling; port the theme layer with parity test; stand up the benchmark/parity/a11y CI harness first; complete the component inventory **and the `modules/ui` triage** (generic / app-specific / hybrid). -- **Phase 1 — Primitives:** icons, typography, button family, status/tag/chip/pill; establish the canonical component pattern. -- **Phase 2 — Behavioral:** Modal, Tooltip, Menu, Tabs, Checkbox, Radio, Switch, inputs/Field, Progress, Avatar, Collapsible. -- **Phase 3 — Long tail:** banners/callout/info, card/section/separator, loader, color/card pickers, code editor, json-visualizer, placeholders, utilities/testing/accessibility. +- ~~**Phase 0 — Foundations**~~ ✅ scaffolding, tooling, theme parity, harnesses. Still open from Phase 0: the CI diff-table workflow and the `modules/ui` triage (moved under Phase 4). +- ~~**Phase 1 — Primitives**~~ ✅ done June 2026 (canonical pattern: `Button.tsx` + data-attribute Sass matrix). +- ~~**Phase 2 — Behavioral**~~ ✅ done June 2026 (Base UI where mapped; see Migration outcomes for exceptions). +- ~~**Phase 3 — Long tail**~~ ✅ done June 2026. Follow-up debt: the a11y fix pass for the 119 `a11y: 'todo'` stories, and the Argos visual-parity triage. - **Phase 4 — Application-level UI:** migrate the generic/hybrid components from `twenty-front/src/modules/ui` per the triage — decouple state, split headless cores, swap each behind its existing `@/ui/...` import path. - **Phase 5 — Hardening & publish:** close gaps; finalize release pipeline; cut `1.0.0`; publish docs. -- **Phase 6 — Cut-over:** dogfood → codemod → swap → remove `twenty-ui`. +- **Phase 6 — Cut-over:** dogfood → codemod → swap → remove `twenty-ui-deprecated`. A component is done only with: stories (all states, light/dark), passing interaction + a11y tests, a passing visual-parity diff, and a within-budget size entry. @@ -316,10 +351,10 @@ a passing visual-parity diff, and a within-budget size entry. ## Open questions -1. Published package name: `twenty-ui` now, renamed to `twenty-ui` at cut-over (Phase 6). -2. Styling: confirm SCSS Modules vs vanilla-extract vs plain CSS Modules. -3. Variants helper: `clsx` + `data-*` vs `cva`. +1. ~~Published package name.~~ **Resolved:** the rename already happened — this package owns `twenty-ui`; the old one is `twenty-ui-deprecated`. +2. ~~Styling: SCSS Modules vs vanilla-extract vs plain CSS Modules.~~ **Resolved:** SCSS Modules, proven across all 192 components. +3. ~~Variants helper: `clsx` + `data-*` vs `cva`.~~ **Resolved:** `clsx` + `data-*` attributes with Sass maps/`@each` (canonical pattern in `Button.module.scss`); `cva` not needed. 4. ~~Visual regression tooling: Chromatic vs self-hosted image snapshots.~~ **Resolved:** Argos (self-hosted at argos.twenty-internal.com). See [Visual regression](#visual-regression). -5. How aggressively to drop `framer-motion` in favor of CSS/Base UI transitions. -6. Scope of `assets` / `testing` / `json-visualizer`: port verbatim or modernize. -7. Where to draw the generic-vs-app-specific line for `modules/ui`, and whether hybrid components live as a headless core in `twenty-ui` with a thin app wrapper in `twenty-front`. +5. ~~How aggressively to drop `framer-motion`.~~ **Resolved:** keep it where animation is the public contract (`utilities/animation`, `AnimatedButton`, `AnimatedCheckmark`, `AnimatedExpandableContainer`, `useProgressAnimation`, `CardContent`); converted to CSS/Base UI transitions everywhere it was an internal micro-transition. +6. ~~Scope of `assets` / `testing` / `json-visualizer`.~~ **Resolved:** ported — assets byte-identical, testing decorators converted to SCSS, json-visualizer recursion verbatim with CSS/Collapsible animations. +7. Where to draw the generic-vs-app-specific line for `modules/ui`, and whether hybrid components live as a headless core in `twenty-ui` with a thin app wrapper in `twenty-front`. **(Still open — Phase 4.)** diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json index e1cd7da9fc..cd8b32d5b7 100644 --- a/packages/twenty-ui/package.json +++ b/packages/twenty-ui/package.json @@ -1,6 +1,25 @@ { "name": "twenty-ui", "version": "0.1.0", + "description": "Twenty's shared React UI component library.", + "license": "AGPL-3.0", + "repository": { + "type": "git", + "url": "https://github.com/twentyhq/twenty.git", + "directory": "packages/twenty-ui" + }, + "homepage": "https://twenty.com", + "bugs": "https://github.com/twentyhq/twenty/issues", + "keywords": [ + "twenty", + "ui", + "react", + "components", + "design-system" + ], + "publishConfig": { + "access": "public" + }, "main": "dist/index.cjs", "module": "dist/index.mjs", "style": "./dist/style.css", @@ -50,6 +69,7 @@ "@base-ui/react": "^1.5.0", "@monaco-editor/react": "^4.7.0", "@radix-ui/colors": "^3.0.0", + "@sniptt/guards": "^0.2.0", "@tabler/icons-react": "^3.31.0", "clsx": "^2.1.1", "date-fns": "^2.30.0", @@ -61,7 +81,6 @@ "react-dom": "^18.2.0", "react-responsive": "^9.0.2", "react-router-dom": "^6.4.4", - "twenty-shared": "workspace:*", "zod": "^4.1.11" }, "peerDependencies": { diff --git a/packages/twenty-ui/src/accessibility/components/VisibilityHidden.module.scss b/packages/twenty-ui/src/accessibility/components/VisibilityHidden.module.scss new file mode 100644 index 0000000000..5f721ddb07 --- /dev/null +++ b/packages/twenty-ui/src/accessibility/components/VisibilityHidden.module.scss @@ -0,0 +1,11 @@ +.root { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} diff --git a/packages/twenty-ui/src/accessibility/components/VisibilityHidden.module.scss.d.ts b/packages/twenty-ui/src/accessibility/components/VisibilityHidden.module.scss.d.ts new file mode 100644 index 0000000000..4d6743c3dc --- /dev/null +++ b/packages/twenty-ui/src/accessibility/components/VisibilityHidden.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly root: 'root'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/accessibility/components/VisibilityHidden.tsx b/packages/twenty-ui/src/accessibility/components/VisibilityHidden.tsx new file mode 100644 index 0000000000..5e826d0565 --- /dev/null +++ b/packages/twenty-ui/src/accessibility/components/VisibilityHidden.tsx @@ -0,0 +1,9 @@ +import styles from './VisibilityHidden.module.scss'; + +export const VisibilityHidden = ({ + children, +}: { + children: React.ReactNode; +}) => { + return {children}; +}; diff --git a/packages/twenty-ui/src/accessibility/components/VisibilityHiddenInput.module.scss b/packages/twenty-ui/src/accessibility/components/VisibilityHiddenInput.module.scss new file mode 100644 index 0000000000..5f721ddb07 --- /dev/null +++ b/packages/twenty-ui/src/accessibility/components/VisibilityHiddenInput.module.scss @@ -0,0 +1,11 @@ +.root { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} diff --git a/packages/twenty-ui/src/accessibility/components/VisibilityHiddenInput.module.scss.d.ts b/packages/twenty-ui/src/accessibility/components/VisibilityHiddenInput.module.scss.d.ts new file mode 100644 index 0000000000..4d6743c3dc --- /dev/null +++ b/packages/twenty-ui/src/accessibility/components/VisibilityHiddenInput.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly root: 'root'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/accessibility/components/VisibilityHiddenInput.tsx b/packages/twenty-ui/src/accessibility/components/VisibilityHiddenInput.tsx new file mode 100644 index 0000000000..ca9d85bbc3 --- /dev/null +++ b/packages/twenty-ui/src/accessibility/components/VisibilityHiddenInput.tsx @@ -0,0 +1,16 @@ +import { clsx } from 'clsx'; +import { forwardRef } from 'react'; + +import styles from './VisibilityHiddenInput.module.scss'; + +// The deprecated Linaria styled.input forwarded refs and accepted all native +// input props, so the port preserves that contract. +export const VisibilityHiddenInput = forwardRef< + HTMLInputElement, + React.ComponentPropsWithoutRef<'input'> +>(({ className, ...props }, ref) => ( + // oxlint-disable-next-line react/jsx-props-no-spreading + +)); + +VisibilityHiddenInput.displayName = 'VisibilityHiddenInput'; diff --git a/packages/twenty-ui/src/accessibility/index.ts b/packages/twenty-ui/src/accessibility/index.ts index a3046804c0..0c3e751b47 100644 --- a/packages/twenty-ui/src/accessibility/index.ts +++ b/packages/twenty-ui/src/accessibility/index.ts @@ -7,4 +7,6 @@ * |___/ */ -export {}; +export { VisibilityHidden } from './components/VisibilityHidden'; +export { VisibilityHiddenInput } from './components/VisibilityHiddenInput'; +export { VISIBILITY_HIDDEN } from './utils/visibility-hidden'; diff --git a/packages/twenty-ui/src/accessibility/utils/visibility-hidden.ts b/packages/twenty-ui/src/accessibility/utils/visibility-hidden.ts new file mode 100644 index 0000000000..0ee9b33dda --- /dev/null +++ b/packages/twenty-ui/src/accessibility/utils/visibility-hidden.ts @@ -0,0 +1,11 @@ +export const VISIBILITY_HIDDEN = ` + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +`; diff --git a/packages/twenty-ui/src/assets/icons/address-book.svg b/packages/twenty-ui/src/assets/icons/address-book.svg new file mode 100644 index 0000000000..817890047d --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/address-book.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/twenty-ui/src/assets/icons/anthropic.svg b/packages/twenty-ui/src/assets/icons/anthropic.svg new file mode 100644 index 0000000000..15b18c9e24 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/anthropic.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/twenty-ui/src/assets/icons/claude.svg b/packages/twenty-ui/src/assets/icons/claude.svg new file mode 100644 index 0000000000..30d6fe9321 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/claude.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/twenty-ui/src/assets/icons/gemini.svg b/packages/twenty-ui/src/assets/icons/gemini.svg new file mode 100644 index 0000000000..5255ae90bb --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/gemini.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/gmail.svg b/packages/twenty-ui/src/assets/icons/gmail.svg new file mode 100644 index 0000000000..3b690353bb --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/gmail.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/google-calendar.svg b/packages/twenty-ui/src/assets/icons/google-calendar.svg new file mode 100644 index 0000000000..5486a4bca2 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/google-calendar.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/google.svg b/packages/twenty-ui/src/assets/icons/google.svg new file mode 100644 index 0000000000..396a3e176d --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/google.svg @@ -0,0 +1 @@ + diff --git a/packages/twenty-ui/src/assets/icons/groq.svg b/packages/twenty-ui/src/assets/icons/groq.svg new file mode 100644 index 0000000000..9e3b4c495b --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/groq.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-array.svg b/packages/twenty-ui/src/assets/icons/illustration-array.svg new file mode 100644 index 0000000000..56b2864ff0 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-array.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-calendar-event.svg b/packages/twenty-ui/src/assets/icons/illustration-calendar-event.svg new file mode 100644 index 0000000000..705ca57ac7 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-calendar-event.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-calendar-time.svg b/packages/twenty-ui/src/assets/icons/illustration-calendar-time.svg new file mode 100644 index 0000000000..6a5b335d29 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-calendar-time.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-currency.svg b/packages/twenty-ui/src/assets/icons/illustration-currency.svg new file mode 100644 index 0000000000..6f801b59dc --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-currency.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-file.svg b/packages/twenty-ui/src/assets/icons/illustration-file.svg new file mode 100644 index 0000000000..bd8988c7e5 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-file.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-json.svg b/packages/twenty-ui/src/assets/icons/illustration-json.svg new file mode 100644 index 0000000000..eb2a6a747c --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-json.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-link.svg b/packages/twenty-ui/src/assets/icons/illustration-link.svg new file mode 100644 index 0000000000..6852612db9 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-link.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-mail.svg b/packages/twenty-ui/src/assets/icons/illustration-mail.svg new file mode 100644 index 0000000000..67787031c9 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-mail.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-many-to-many.svg b/packages/twenty-ui/src/assets/icons/illustration-many-to-many.svg new file mode 100644 index 0000000000..06f6a5539e --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-many-to-many.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-map.svg b/packages/twenty-ui/src/assets/icons/illustration-map.svg new file mode 100644 index 0000000000..a78cae3ae2 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-map.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-numbers.svg b/packages/twenty-ui/src/assets/icons/illustration-numbers.svg new file mode 100644 index 0000000000..238a1c474b --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-numbers.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-one-to-many.svg b/packages/twenty-ui/src/assets/icons/illustration-one-to-many.svg new file mode 100644 index 0000000000..bc0c08aa51 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-one-to-many.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-one-to-one.svg b/packages/twenty-ui/src/assets/icons/illustration-one-to-one.svg new file mode 100644 index 0000000000..1dcdff806d --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-one-to-one.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-phone.svg b/packages/twenty-ui/src/assets/icons/illustration-phone.svg new file mode 100644 index 0000000000..e286b05178 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-phone.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-setting.svg b/packages/twenty-ui/src/assets/icons/illustration-setting.svg new file mode 100644 index 0000000000..007c16e28b --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-setting.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-star.svg b/packages/twenty-ui/src/assets/icons/illustration-star.svg new file mode 100644 index 0000000000..9a0c714b5d --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-star.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-tag.svg b/packages/twenty-ui/src/assets/icons/illustration-tag.svg new file mode 100644 index 0000000000..9ca0212269 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-tag.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-tags.svg b/packages/twenty-ui/src/assets/icons/illustration-tags.svg new file mode 100644 index 0000000000..0174ac4fe9 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-tags.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-text.svg b/packages/twenty-ui/src/assets/icons/illustration-text.svg new file mode 100644 index 0000000000..3e35ec3c7e --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-text.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-toggle.svg b/packages/twenty-ui/src/assets/icons/illustration-toggle.svg new file mode 100644 index 0000000000..ad2d652673 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-toggle.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-uid.svg b/packages/twenty-ui/src/assets/icons/illustration-uid.svg new file mode 100644 index 0000000000..4affb07a32 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-uid.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/illustration-user.svg b/packages/twenty-ui/src/assets/icons/illustration-user.svg new file mode 100644 index 0000000000..05c928e0fd --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/illustration-user.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/lock.svg b/packages/twenty-ui/src/assets/icons/lock.svg new file mode 100644 index 0000000000..6fd1e54643 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/lock.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/many-to-one.svg b/packages/twenty-ui/src/assets/icons/many-to-one.svg new file mode 100644 index 0000000000..1ee3be95df --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/many-to-one.svg @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/packages/twenty-ui/src/assets/icons/microsoft-calendar.svg b/packages/twenty-ui/src/assets/icons/microsoft-calendar.svg new file mode 100644 index 0000000000..fcb25e21bb --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/microsoft-calendar.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/twenty-ui/src/assets/icons/microsoft-outlook.svg b/packages/twenty-ui/src/assets/icons/microsoft-outlook.svg new file mode 100644 index 0000000000..6449f4bef0 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/microsoft-outlook.svg @@ -0,0 +1 @@ + diff --git a/packages/twenty-ui/src/assets/icons/microsoft.svg b/packages/twenty-ui/src/assets/icons/microsoft.svg new file mode 100644 index 0000000000..404ad3eb77 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/microsoft.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/packages/twenty-ui/src/assets/icons/mistral.svg b/packages/twenty-ui/src/assets/icons/mistral.svg new file mode 100644 index 0000000000..36d5da9f6f --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/mistral.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/twenty-ui/src/assets/icons/openai.svg b/packages/twenty-ui/src/assets/icons/openai.svg new file mode 100644 index 0000000000..000f65c34a --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/openai.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/twenty-ui/src/assets/icons/trash-x-off.svg b/packages/twenty-ui/src/assets/icons/trash-x-off.svg new file mode 100644 index 0000000000..24e730edf3 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/trash-x-off.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/packages/twenty-ui/src/assets/icons/twenty-star-filled.svg b/packages/twenty-ui/src/assets/icons/twenty-star-filled.svg new file mode 100644 index 0000000000..b9b018fb79 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/twenty-star-filled.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/twenty-ui/src/assets/icons/twenty-star.svg b/packages/twenty-ui/src/assets/icons/twenty-star.svg new file mode 100644 index 0000000000..9d2200996c --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/twenty-star.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/twenty-ui/src/assets/icons/xai.svg b/packages/twenty-ui/src/assets/icons/xai.svg new file mode 100644 index 0000000000..ccd22443c4 --- /dev/null +++ b/packages/twenty-ui/src/assets/icons/xai.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/twenty-ui/src/assets/misc/list-view-grip.svg b/packages/twenty-ui/src/assets/misc/list-view-grip.svg new file mode 100644 index 0000000000..b8a82f167f --- /dev/null +++ b/packages/twenty-ui/src/assets/misc/list-view-grip.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/twenty-ui/src/assets/themes/dark-noise.jpg b/packages/twenty-ui/src/assets/themes/dark-noise.jpg new file mode 100644 index 0000000000..6fc95715e9 Binary files /dev/null and b/packages/twenty-ui/src/assets/themes/dark-noise.jpg differ diff --git a/packages/twenty-ui/src/assets/themes/light-noise.png b/packages/twenty-ui/src/assets/themes/light-noise.png new file mode 100644 index 0000000000..d7b3bc2c06 Binary files /dev/null and b/packages/twenty-ui/src/assets/themes/light-noise.png differ diff --git a/packages/twenty-ui/src/components/Pill/Pill.module.scss b/packages/twenty-ui/src/components/Pill/Pill.module.scss new file mode 100644 index 0000000000..b3638527c0 --- /dev/null +++ b/packages/twenty-ui/src/components/Pill/Pill.module.scss @@ -0,0 +1,15 @@ +.pill { + align-items: center; + background: var(--t-background-transparent-light); + border-radius: var(--t-border-radius-pill); + color: var(--t-font-color-light); + display: inline-flex; + font-size: var(--t-font-size-xs); + font-style: normal; + font-weight: var(--t-font-weight-medium); + gap: var(--t-spacing-1); + height: var(--t-spacing-4); + justify-content: flex-end; + line-height: var(--t-text-line-height-lg); + padding: 0 var(--t-spacing-2); +} diff --git a/packages/twenty-ui/src/components/Pill/Pill.module.scss.d.ts b/packages/twenty-ui/src/components/Pill/Pill.module.scss.d.ts new file mode 100644 index 0000000000..a576c6a318 --- /dev/null +++ b/packages/twenty-ui/src/components/Pill/Pill.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly pill: 'pill'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/components/Pill/Pill.tsx b/packages/twenty-ui/src/components/Pill/Pill.tsx new file mode 100644 index 0000000000..894639dc12 --- /dev/null +++ b/packages/twenty-ui/src/components/Pill/Pill.tsx @@ -0,0 +1,19 @@ +import { type IconComponent } from '@ui/display'; +import { clsx } from 'clsx'; + +import styles from './Pill.module.scss'; + +type PillProps = { + className?: string; + label?: string; + Icon?: IconComponent; +}; + +export const Pill = ({ className, label, Icon }: PillProps) => { + return ( + + {Icon && } + {label} + + ); +}; diff --git a/packages/twenty-ui/src/components/Pill/__stories__/Pill.stories.ts b/packages/twenty-ui/src/components/Pill/__stories__/Pill.stories.ts new file mode 100644 index 0000000000..4dbd473842 --- /dev/null +++ b/packages/twenty-ui/src/components/Pill/__stories__/Pill.stories.ts @@ -0,0 +1,21 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { Pill } from '@ui/components/Pill/Pill'; +import { ComponentDecorator } from '../../../testing/decorators/ComponentDecorator'; + +const meta: Meta = { + title: 'UI/Display/Pill', + component: Pill, + decorators: [ComponentDecorator], + args: { + label: 'Soon', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, +}; diff --git a/packages/twenty-ui/src/components/avatar-or-icon/AvatarOrIcon.module.scss b/packages/twenty-ui/src/components/avatar-or-icon/AvatarOrIcon.module.scss new file mode 100644 index 0000000000..1843b2a50e --- /dev/null +++ b/packages/twenty-ui/src/components/avatar-or-icon/AvatarOrIcon.module.scss @@ -0,0 +1,21 @@ +.iconWithBackgroundContainer { + display: flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + border-radius: 4px; + background-color: var( + --avatar-or-icon-background, + var(--t-background-inverted-secondary) + ); +} + +.wrapper { + cursor: inherit; + display: flex; + + &[data-clickable] { + cursor: pointer; + } +} diff --git a/packages/twenty-ui/src/components/avatar-or-icon/AvatarOrIcon.module.scss.d.ts b/packages/twenty-ui/src/components/avatar-or-icon/AvatarOrIcon.module.scss.d.ts new file mode 100644 index 0000000000..545cc9c2ae --- /dev/null +++ b/packages/twenty-ui/src/components/avatar-or-icon/AvatarOrIcon.module.scss.d.ts @@ -0,0 +1,5 @@ +declare const classNames: { + readonly iconWithBackgroundContainer: 'iconWithBackgroundContainer'; + readonly wrapper: 'wrapper'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/components/avatar-or-icon/AvatarOrIcon.tsx b/packages/twenty-ui/src/components/avatar-or-icon/AvatarOrIcon.tsx new file mode 100644 index 0000000000..7efd3ef362 --- /dev/null +++ b/packages/twenty-ui/src/components/avatar-or-icon/AvatarOrIcon.tsx @@ -0,0 +1,91 @@ +import { Avatar } from '@ui/display/avatar/components/Avatar'; +import { type AvatarType } from '@ui/display/avatar/types/AvatarType'; +import { type IconComponent } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; +import { type Nullable } from '@ui/utilities'; +import { useContext } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +import styles from './AvatarOrIcon.module.scss'; + +export type AvatarOrIconProps = { + placeholder?: string; + avatarUrl?: string; + avatarType?: Nullable; + Icon?: IconComponent; + IconColor?: string; + IconBackgroundColor?: string; + isIconInverted?: boolean; + placeholderColorSeed?: string; + onClick?: () => void; +}; + +export const AvatarOrIcon = ({ + Icon, + placeholderColorSeed, + avatarType, + avatarUrl, + placeholder, + isIconInverted = false, + IconColor, + IconBackgroundColor, + onClick, +}: AvatarOrIconProps) => { + const { theme } = useContext(ThemeContext); + + if (!isDefined(Icon)) { + return ( + + ); + } + + const isClickable = isDefined(onClick); + + if (isIconInverted || isDefined(IconBackgroundColor)) { + return ( +
+
+ +
+
+ ); + } + + return ( +
+ +
+ ); +}; diff --git a/packages/twenty-ui/src/components/avatar-or-icon/__stories__/AvatarOrIcon.stories.tsx b/packages/twenty-ui/src/components/avatar-or-icon/__stories__/AvatarOrIcon.stories.tsx new file mode 100644 index 0000000000..8b26d33d1a --- /dev/null +++ b/packages/twenty-ui/src/components/avatar-or-icon/__stories__/AvatarOrIcon.stories.tsx @@ -0,0 +1,64 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { IconBuildingSkyscraper, IconUser } from '@ui/display'; +import { ComponentDecorator, JotaiRootDecorator } from '@ui/testing'; +import { AvatarOrIcon } from '../AvatarOrIcon'; + +const meta: Meta = { + title: 'UI/Components/AvatarOrIcon', + component: AvatarOrIcon, + decorators: [ComponentDecorator, JotaiRootDecorator], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + placeholder: 'JD', + placeholderColorSeed: 'John Doe', + }, +}; + +export const WithAvatar: Story = { + args: { + avatarUrl: 'https://i.pravatar.cc/300', + placeholder: 'JD', + placeholderColorSeed: 'John Doe', + }, +}; + +export const WithIcon: Story = { + args: { + Icon: IconUser, + }, +}; + +export const WithIconBackground: Story = { + args: { + Icon: IconBuildingSkyscraper, + isIconInverted: true, + }, +}; + +export const WithInvertedIcon: Story = { + args: { + Icon: IconUser, + isIconInverted: true, + }, +}; + +export const Clickable: Story = { + args: { + placeholder: 'JD', + placeholderColorSeed: 'John Doe', + onClick: () => alert('AvatarOrIcon clicked'), + }, +}; + +export const ClickableIcon: Story = { + args: { + Icon: IconBuildingSkyscraper, + isIconInverted: true, + onClick: () => alert('Icon AvatarOrIcon clicked'), + }, +}; diff --git a/packages/twenty-ui/src/components/chip/Chip.module.scss b/packages/twenty-ui/src/components/chip/Chip.module.scss new file mode 100644 index 0000000000..b1cb285c22 --- /dev/null +++ b/packages/twenty-ui/src/components/chip/Chip.module.scss @@ -0,0 +1,110 @@ +.chip { + --chip-horizontal-padding: var(--t-spacing-1); + --chip-vertical-padding: var(--t-spacing-1); + + // Height and max-width are sized against the content box. + box-sizing: content-box; + text-decoration: none; + align-items: center; + cursor: inherit; + display: inline-flex; + justify-content: flex-start; + gap: var(--t-spacing-1); + max-width: 100%; + overflow: hidden; + padding: var(--chip-vertical-padding) var(--chip-horizontal-padding); + user-select: none; + font-weight: inherit; + background-color: inherit; + border: none; + border-radius: var(--t-border-radius-sm); + + & > svg { + flex-shrink: 0; + } +} + +.sizeLarge { + height: var(--t-spacing-4); +} + +.sizeSmall { + height: var(--t-spacing-3); +} + +.accentTextPrimary { + color: var(--t-font-color-primary); +} + +.accentTextSecondary { + color: var(--t-font-color-secondary); +} + +// Disabled color wins over accent colors, matching the Linaria ternary order. +.disabled { + color: var(--t-font-color-light); +} + +.fontMedium { + font-weight: var(--t-font-weight-medium); +} + +.cursorPointer { + cursor: pointer; +} + +.cursorNotAllowed { + cursor: not-allowed; +} + +.backgroundHighlighted { + background-color: var(--t-background-transparent-light); + + &:hover { + background-color: var(--t-background-transparent-medium); + } + + &:active { + background-color: var(--t-background-transparent-strong); + } +} + +.backgroundStatic { + background-color: var(--t-background-transparent-light); + + &:hover { + background-color: var(--t-background-transparent-light); + } + + &:active { + background-color: var(--t-background-transparent-light); + } +} + +// Applied only when variant is regular, clickable and not disabled. +.interactiveRegular { + &:hover { + background-color: var(--t-background-transparent-light); + } + + &:active { + background-color: var(--t-background-transparent-medium); + } +} + +.hasMaxWidth { + max-width: calc(var(--chip-max-width) - 2 * var(--chip-horizontal-padding)); +} + +.paddingLeftNone { + padding-left: var(--t-spacing-0); +} + +.emptyLabel { + color: var(--t-font-color-tertiary); +} + +.rightComponentDivider { + align-self: stretch; + border-left: 1px solid var(--t-border-color-light); +} diff --git a/packages/twenty-ui/src/components/chip/Chip.module.scss.d.ts b/packages/twenty-ui/src/components/chip/Chip.module.scss.d.ts new file mode 100644 index 0000000000..eb38f2abac --- /dev/null +++ b/packages/twenty-ui/src/components/chip/Chip.module.scss.d.ts @@ -0,0 +1,19 @@ +declare const classNames: { + readonly chip: 'chip'; + readonly sizeLarge: 'sizeLarge'; + readonly sizeSmall: 'sizeSmall'; + readonly accentTextPrimary: 'accentTextPrimary'; + readonly accentTextSecondary: 'accentTextSecondary'; + readonly disabled: 'disabled'; + readonly fontMedium: 'fontMedium'; + readonly cursorPointer: 'cursorPointer'; + readonly cursorNotAllowed: 'cursorNotAllowed'; + readonly backgroundHighlighted: 'backgroundHighlighted'; + readonly backgroundStatic: 'backgroundStatic'; + readonly interactiveRegular: 'interactiveRegular'; + readonly hasMaxWidth: 'hasMaxWidth'; + readonly paddingLeftNone: 'paddingLeftNone'; + readonly emptyLabel: 'emptyLabel'; + readonly rightComponentDivider: 'rightComponentDivider'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/components/chip/Chip.tsx b/packages/twenty-ui/src/components/chip/Chip.tsx new file mode 100644 index 0000000000..ac91fc7850 --- /dev/null +++ b/packages/twenty-ui/src/components/chip/Chip.tsx @@ -0,0 +1,149 @@ +import { type ReactNode } from 'react'; + +import { isNonEmptyString } from '@sniptt/guards'; +import { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip'; +import { clsx } from 'clsx'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +import styles from './Chip.module.scss'; + +export enum ChipSize { + Large = 'large', + Small = 'small', +} + +export enum ChipAccent { + TextPrimary = 'text-primary', + TextSecondary = 'text-secondary', +} + +export enum ChipVariant { + Highlighted = 'highlighted', + Regular = 'regular', + Transparent = 'transparent', + Rounded = 'rounded', + Static = 'static', +} + +export type ChipProps = { + size?: ChipSize; + disabled?: boolean; + clickable?: boolean; + label: string; + tooltipLabel?: string; + alwaysShowTooltip?: boolean; + isLabelHidden?: boolean; + isBold?: boolean; + maxWidth?: number; + variant?: ChipVariant; + accent?: ChipAccent; + leftComponent?: ReactNode | null; + rightComponent?: (() => ReactNode) | ReactNode | null; + rightComponentDivider?: boolean; + className?: string; + forceEmptyText?: boolean; + emptyLabel?: string; +}; + +const renderRightComponent = ( + rightComponent: (() => ReactNode) | ReactNode | null, + rightComponentDivider?: boolean, +) => { + if (!rightComponent) { + return null; + } + + const rendered = + typeof rightComponent === 'function' ? rightComponent() : rightComponent; + + if (rightComponentDivider === true) { + return ( + <> +
+ {rendered} + + ); + } + + return rendered; +}; + +export const Chip = ({ + size = ChipSize.Small, + label, + tooltipLabel, + alwaysShowTooltip = false, + isLabelHidden = false, + isBold = false, + disabled = false, + clickable = true, + variant = ChipVariant.Regular, + leftComponent = null, + rightComponent = null, + rightComponentDivider = false, + accent = ChipAccent.TextPrimary, + className, + maxWidth, + forceEmptyText = false, + emptyLabel = 'Untitled', +}: ChipProps) => { + // Cursor precedence mirrors the Linaria ternary: + // transparent > clickable > disabled > inherit. + const cursorClass = + variant === ChipVariant.Transparent + ? undefined + : clickable + ? styles.cursorPointer + : disabled + ? styles.cursorNotAllowed + : undefined; + + const backgroundClass = + variant === ChipVariant.Highlighted + ? styles.backgroundHighlighted + : variant === ChipVariant.Static + ? styles.backgroundStatic + : variant === ChipVariant.Regular && !disabled && clickable + ? styles.interactiveRegular + : undefined; + + return ( +
+ {leftComponent} + {!isLabelHidden && isDefined(label) && isNonEmptyString(label) ? ( + + ) : !forceEmptyText && !isLabelHidden ? ( +
{emptyLabel}
+ ) : ( + '' + )} + {renderRightComponent(rightComponent, rightComponentDivider)} +
+ ); +}; diff --git a/packages/twenty-ui/src/components/chip/LinkChip.module.scss b/packages/twenty-ui/src/components/chip/LinkChip.module.scss new file mode 100644 index 0000000000..e5da170d02 --- /dev/null +++ b/packages/twenty-ui/src/components/chip/LinkChip.module.scss @@ -0,0 +1,11 @@ +.linkContainer { + display: inline-flex; + min-width: 0; + vertical-align: middle; + + & > a { + max-width: 100%; + min-width: 0; + text-decoration: none; + } +} diff --git a/packages/twenty-ui/src/components/chip/LinkChip.module.scss.d.ts b/packages/twenty-ui/src/components/chip/LinkChip.module.scss.d.ts new file mode 100644 index 0000000000..b084afd07e --- /dev/null +++ b/packages/twenty-ui/src/components/chip/LinkChip.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly linkContainer: 'linkContainer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/components/chip/LinkChip.tsx b/packages/twenty-ui/src/components/chip/LinkChip.tsx new file mode 100644 index 0000000000..d055957523 --- /dev/null +++ b/packages/twenty-ui/src/components/chip/LinkChip.tsx @@ -0,0 +1,82 @@ +import { + Chip, + ChipAccent, + type ChipProps, + ChipSize, + ChipVariant, +} from '@ui/components/chip/Chip'; +import { LINK_CHIP_CLICK_OUTSIDE_ID } from '@ui/components/chip/constants/LinkChipClickOutsideId'; +import { type TriggerEventType, useMouseDownNavigation } from '@ui/utilities'; +import { type MouseEvent } from 'react'; +import { Link } from 'react-router-dom'; + +import styles from './LinkChip.module.scss'; + +export type LinkChipProps = Omit< + ChipProps, + 'onClick' | 'disabled' | 'clickable' +> & { + to: string; + onClick?: (event: MouseEvent) => void; + onMouseDown?: (event: MouseEvent) => void; + triggerEvent?: TriggerEventType; + target?: '_blank' | '_self'; +}; + +export const LinkChip = ({ + to, + size = ChipSize.Small, + label, + isLabelHidden = false, + isBold = false, + variant = ChipVariant.Regular, + leftComponent = null, + rightComponent = null, + rightComponentDivider = false, + accent = ChipAccent.TextPrimary, + className, + maxWidth, + onClick, + triggerEvent, + target, + emptyLabel, +}: LinkChipProps) => { + const { onClick: onClickHandler, onMouseDown: onMouseDownHandler } = + useMouseDownNavigation({ + to: to, + onClick: onClick, + triggerEvent, + }); + + return ( + + { + event.stopPropagation(); + onClickHandler(event); + }} + onMouseDown={onMouseDownHandler} + data-click-outside-id={LINK_CHIP_CLICK_OUTSIDE_ID} + target={target} + rel={target === '_blank' ? 'noopener noreferrer' : undefined} + > + + + + ); +}; diff --git a/packages/twenty-ui/src/components/chip/__stories__/Chip.stories.tsx b/packages/twenty-ui/src/components/chip/__stories__/Chip.stories.tsx new file mode 100644 index 0000000000..1a4870ed7b --- /dev/null +++ b/packages/twenty-ui/src/components/chip/__stories__/Chip.stories.tsx @@ -0,0 +1,204 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { IconUser, IconX } from '@ui/display'; +import { Avatar } from '@ui/display/avatar/components/Avatar'; + +import { + CatalogDecorator, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; + +import { Chip, ChipAccent, ChipSize, ChipVariant } from '../Chip'; + +const meta: Meta = { + title: 'UI/Components/Chip/Chip', + component: Chip, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'Chip test', + size: ChipSize.Small, + variant: ChipVariant.Highlighted, + accent: ChipAccent.TextPrimary, + disabled: false, + clickable: true, + maxWidth: 200, + }, + decorators: [ComponentDecorator], +}; + +export const WithLeftAvatar: Story = { + args: { + label: 'John Doe', + clickable: true, + variant: ChipVariant.Regular, + leftComponent: ( + + ), + }, + decorators: [ComponentDecorator], +}; + +export const WithLeftIcon: Story = { + args: { + label: 'Company', + clickable: true, + variant: ChipVariant.Regular, + leftComponent: , + }, + decorators: [ComponentDecorator], +}; + +export const EmptyLabel: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + label: '', + clickable: true, + variant: ChipVariant.Regular, + leftComponent: ( + + ), + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: { clickable: true, label: 'Hello' }, + argTypes: { + size: { control: false }, + variant: { control: false }, + accent: { control: false }, + disabled: { control: false }, + className: { control: false }, + rightComponent: { control: false }, + leftComponent: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.active'] }, + catalog: { + dimensions: [ + { + name: 'variants', + values: Object.values(ChipVariant), + props: (variant: ChipVariant) => ({ variant }), + }, + { + name: 'sizes', + values: Object.values(ChipSize), + props: (size: ChipSize) => ({ size }), + }, + { + name: 'states', + values: ['default', 'hover', 'active', 'disabled'], + props: (state: string) => { + switch (state) { + case 'hover': + case 'active': + return { className: state }; + case 'disabled': + return { disabled: true }; + default: + return {}; + } + }, + }, + { + name: 'accents', + values: Object.values(ChipAccent), + props: (accent: ChipAccent) => ({ accent }), + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; + +export const WithAvatarCatalog: CatalogStory = { + args: { + clickable: true, + label: 'John Doe', + leftComponent: ( + + ), + }, + argTypes: { + size: { control: false }, + variant: { control: false }, + accent: { control: false }, + disabled: { control: false }, + className: { control: false }, + rightComponent: { control: false }, + leftComponent: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.active'] }, + catalog: { + dimensions: [ + { + name: 'variants', + values: Object.values(ChipVariant), + props: (variant: ChipVariant) => ({ variant }), + }, + { + name: 'sizes', + values: Object.values(ChipSize), + props: (size: ChipSize) => ({ size }), + }, + { + name: 'states', + values: ['default', 'hover', 'active', 'disabled'], + props: (state: string) => { + switch (state) { + case 'hover': + case 'active': + return { className: state }; + case 'disabled': + return { disabled: true }; + default: + return {}; + } + }, + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; + +export const WithRightComponentDivider: Story = { + args: { + label: 'document.pdf', + variant: ChipVariant.Static, + clickable: false, + leftComponent: ( + + ), + rightComponent: , + rightComponentDivider: true, + }, + decorators: [ComponentDecorator], +}; diff --git a/packages/twenty-ui/src/components/chip/__stories__/LinkChip.stories.tsx b/packages/twenty-ui/src/components/chip/__stories__/LinkChip.stories.tsx new file mode 100644 index 0000000000..e6956cf1f7 --- /dev/null +++ b/packages/twenty-ui/src/components/chip/__stories__/LinkChip.stories.tsx @@ -0,0 +1,190 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { IconUser, IconX } from '@ui/display'; +import { Avatar } from '@ui/display/avatar/components/Avatar'; +import { + CatalogDecorator, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; +import { MemoryRouter } from 'react-router-dom'; + +import { ChipAccent, ChipSize, ChipVariant } from '../Chip'; +import { LinkChip } from '../LinkChip'; + +const meta: Meta = { + title: 'UI/Components/Chip/LinkChip', + component: LinkChip, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'Link Chip', + to: '/example', + size: ChipSize.Small, + variant: ChipVariant.Regular, + accent: ChipAccent.TextPrimary, + }, + decorators: [ComponentDecorator], +}; + +export const WithAvatar: Story = { + args: { + label: 'John Doe', + to: '/users/john-doe', + variant: ChipVariant.Regular, + leftComponent: ( + + ), + }, + decorators: [ComponentDecorator], +}; + +export const WithIcon: Story = { + args: { + label: 'Company', + to: '/companies/1', + variant: ChipVariant.Regular, + leftComponent: , + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: { label: 'Link Chip', to: '/example' }, + argTypes: { + size: { control: false }, + variant: { control: false }, + accent: { control: false }, + className: { control: false }, + rightComponent: { control: false }, + leftComponent: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.active'] }, + catalog: { + dimensions: [ + { + name: 'variants', + values: Object.values(ChipVariant), + props: (variant: ChipVariant) => ({ variant }), + }, + { + name: 'sizes', + values: Object.values(ChipSize), + props: (size: ChipSize) => ({ size }), + }, + { + name: 'states', + values: ['default', 'hover', 'active'], + props: (state: string) => { + switch (state) { + case 'hover': + case 'active': + return { className: state }; + default: + return {}; + } + }, + }, + { + name: 'accents', + values: Object.values(ChipAccent), + props: (accent: ChipAccent) => ({ accent }), + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; + +export const WithAvatarCatalog: CatalogStory = { + args: { + label: 'John Doe', + to: '/users/john-doe', + leftComponent: ( + + ), + }, + argTypes: { + size: { control: false }, + variant: { control: false }, + accent: { control: false }, + className: { control: false }, + rightComponent: { control: false }, + leftComponent: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.active'] }, + catalog: { + dimensions: [ + { + name: 'variants', + values: Object.values(ChipVariant), + props: (variant: ChipVariant) => ({ variant }), + }, + { + name: 'sizes', + values: Object.values(ChipSize), + props: (size: ChipSize) => ({ size }), + }, + { + name: 'states', + values: ['default', 'hover', 'active'], + props: (state: string) => { + switch (state) { + case 'hover': + case 'active': + return { className: state }; + default: + return {}; + } + }, + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; + +export const WithRightComponentDivider: Story = { + args: { + label: 'document.pdf', + to: '/files/document.pdf', + variant: ChipVariant.Static, + leftComponent: ( + + ), + rightComponent: , + rightComponentDivider: true, + }, + decorators: [ComponentDecorator], +}; diff --git a/packages/twenty-ui/src/components/chip/constants/LinkChipClickOutsideId.ts b/packages/twenty-ui/src/components/chip/constants/LinkChipClickOutsideId.ts new file mode 100644 index 0000000000..1e2a2a55e2 --- /dev/null +++ b/packages/twenty-ui/src/components/chip/constants/LinkChipClickOutsideId.ts @@ -0,0 +1 @@ +export const LINK_CHIP_CLICK_OUTSIDE_ID = 'link-chip-click-outside-id'; diff --git a/packages/twenty-ui/src/components/index.ts b/packages/twenty-ui/src/components/index.ts index a3046804c0..cecc41633d 100644 --- a/packages/twenty-ui/src/components/index.ts +++ b/packages/twenty-ui/src/components/index.ts @@ -7,4 +7,13 @@ * |___/ */ -export {}; +export type { AvatarOrIconProps } from './avatar-or-icon/AvatarOrIcon'; +export { AvatarOrIcon } from './avatar-or-icon/AvatarOrIcon'; +export type { ChipProps } from './chip/Chip'; +export { ChipSize, ChipAccent, ChipVariant, Chip } from './chip/Chip'; +export { LINK_CHIP_CLICK_OUTSIDE_ID } from './chip/constants/LinkChipClickOutsideId'; +export type { LinkChipProps } from './chip/LinkChip'; +export { LinkChip } from './chip/LinkChip'; +export { Pill } from './Pill/Pill'; +export type { TagColor } from './tag/Tag'; +export { Tag } from './tag/Tag'; diff --git a/packages/twenty-ui/src/components/tag/Tag.module.scss b/packages/twenty-ui/src/components/tag/Tag.module.scss new file mode 100644 index 0000000000..63ffa10780 --- /dev/null +++ b/packages/twenty-ui/src/components/tag/Tag.module.scss @@ -0,0 +1,55 @@ +.tag { + box-sizing: content-box; + align-items: center; + background: var(--tag-background); + border-radius: var(--t-border-radius-sm); + color: var(--tag-text); + display: inline-flex; + font-size: var(--t-font-size-md); + font-style: normal; + font-weight: var(--t-font-weight-regular); + height: var(--t-spacing-5); + margin: 0; + overflow: hidden; + padding: 0 var(--t-spacing-2); + border: none; + gap: var(--t-spacing-1); + // 'none' is not a valid min-width value; kept for byte-equivalence with the + // deprecated Linaria styles where the declaration was equally inert. + min-width: none; +} + +.weightMedium { + font-weight: var(--t-font-weight-medium); +} + +.variantOutline { + border: 1px dashed var(--t-border-color-strong); +} + +.variantBorder { + border: 1px solid var(--t-border-color-strong); +} + +.preventShrink { + min-width: fit-content; +} + +.preventPadding { + padding: 0; +} + +.content { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.nonShrinkableText { + white-space: nowrap; + width: fit-content; +} + +.iconContainer { + display: flex; +} diff --git a/packages/twenty-ui/src/components/tag/Tag.module.scss.d.ts b/packages/twenty-ui/src/components/tag/Tag.module.scss.d.ts new file mode 100644 index 0000000000..cce3a01c58 --- /dev/null +++ b/packages/twenty-ui/src/components/tag/Tag.module.scss.d.ts @@ -0,0 +1,12 @@ +declare const classNames: { + readonly tag: 'tag'; + readonly weightMedium: 'weightMedium'; + readonly variantOutline: 'variantOutline'; + readonly variantBorder: 'variantBorder'; + readonly preventShrink: 'preventShrink'; + readonly preventPadding: 'preventPadding'; + readonly content: 'content'; + readonly nonShrinkableText: 'nonShrinkableText'; + readonly iconContainer: 'iconContainer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/components/tag/Tag.tsx b/packages/twenty-ui/src/components/tag/Tag.tsx new file mode 100644 index 0000000000..7b2439c0c6 --- /dev/null +++ b/packages/twenty-ui/src/components/tag/Tag.tsx @@ -0,0 +1,87 @@ +import { type IconComponent } from '@ui/display/icon/types/IconComponent'; +import { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip'; +import { type ThemeColor } from '@ui/theme'; +import { ThemeContext, themeCssVariables } from '@ui/theme-constants'; +import { clsx } from 'clsx'; +import { useContext } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +import styles from './Tag.module.scss'; + +type TagWeight = 'regular' | 'medium'; +type TagVariant = 'solid' | 'outline' | 'border'; +export type TagColor = ThemeColor | 'transparent'; + +type TagProps = { + className?: string; + color: TagColor; + text: string; + Icon?: IconComponent; + onClick?: () => void; + weight?: TagWeight; + variant?: TagVariant; + preventShrink?: boolean; + preventPadding?: boolean; +}; + +export const Tag = ({ + className, + color, + text, + Icon, + onClick, + weight = 'regular', + variant = 'solid', + preventShrink, + preventPadding, +}: TagProps) => { + const { theme } = useContext(ThemeContext); + + const tagBackground = + color === 'transparent' + ? 'transparent' + : (themeCssVariables.tag.background[color] ?? + themeCssVariables.tag.background.gray); + + const tagText = + color === 'transparent' + ? themeCssVariables.font.color.secondary + : (themeCssVariables.tag.text[color] ?? + themeCssVariables.font.color.secondary); + + return ( + + {isDefined(Icon) ? ( +
+ +
+ ) : ( + <> + )} + {preventShrink ? ( + {text} + ) : ( + + + + )} +
+ ); +}; diff --git a/packages/twenty-ui/src/components/tag/__stories__/Tag.stories.tsx b/packages/twenty-ui/src/components/tag/__stories__/Tag.stories.tsx new file mode 100644 index 0000000000..ce27486d44 --- /dev/null +++ b/packages/twenty-ui/src/components/tag/__stories__/Tag.stories.tsx @@ -0,0 +1,113 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { expect, fn, userEvent, within } from 'storybook/test'; + +import { IconUser } from '@ui/display/icon/components/TablerIcons'; +import { + CatalogDecorator, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; +import { type ThemeColor, MAIN_COLOR_NAMES } from '@ui/theme'; + +import { Tag } from '../Tag'; + +const meta: Meta = { + title: 'UI/Display/Tag/Tag', + component: Tag, + args: { + text: 'Urgent', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + color: 'red', + onClick: fn(), + }, + decorators: [ComponentDecorator], + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const tag = canvas.getByText('Urgent'); + + await userEvent.click(tag); + await expect(args.onClick).toHaveBeenCalled(); + }, +}; + +export const WithLongText: Story = { + decorators: [ComponentDecorator], + args: { + color: 'green', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + container: { width: 100 }, + }, +}; + +export const WithIcon: Story = { + decorators: [ComponentDecorator], + args: { + color: 'green', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', + Icon: IconUser, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + container: { width: 100 }, + }, +}; + +export const DontShrink: Story = { + decorators: [ComponentDecorator], + args: { + color: 'green', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', + preventShrink: true, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + container: { width: 100 }, + }, +}; + +export const Catalog: CatalogStory = { + argTypes: { + color: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + catalog: { + dimensions: [ + { + name: 'colors', + values: MAIN_COLOR_NAMES, + props: (color: ThemeColor) => ({ color }), + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; + +export const EmptyTag: Story = { + decorators: [ComponentDecorator], + args: { + color: 'transparent', + text: 'No Value', + variant: 'outline', + weight: 'medium', + }, + parameters: { + container: { width: 'auto' }, + }, +}; diff --git a/packages/twenty-ui/src/display/avatar/components/Avatar.module.scss b/packages/twenty-ui/src/display/avatar/components/Avatar.module.scss new file mode 100644 index 0000000000..a34d225baa --- /dev/null +++ b/packages/twenty-ui/src/display/avatar/components/Avatar.module.scss @@ -0,0 +1,66 @@ +// Size values mirror AVATAR_PROPERTIES_BY_SIZE (constants/AvatarPropertiesBySize.ts) +.root { + align-items: center; + flex-shrink: 0; + overflow: hidden; + user-select: none; + border-radius: 2px; + border: var(--avatar-border, none); + box-sizing: border-box; + display: flex; + justify-content: center; + color: var(--avatar-color); + background: var(--avatar-background); + + &[data-type='icon'] { + border-radius: 4px; + } + + &[data-type='rounded'] { + border-radius: 50%; + } + + &[data-clickable]:hover { + box-shadow: 0 0 0 4px var(--t-background-transparent-light); + } +} + +.xl { + font-size: 16px; + height: 40px; + width: 40px; +} + +.lg { + font-size: 13px; + height: 24px; + width: 24px; +} + +.md { + font-size: 12px; + height: 16px; + width: 16px; +} + +.sm { + font-size: 10px; + height: 14px; + width: 14px; +} + +.xs { + font-size: 8px; + height: 12px; + width: 12px; +} + +.image { + height: 100%; + object-fit: cover; + width: 100%; +} + +.placeholderChar { + font-weight: 500; +} diff --git a/packages/twenty-ui/src/display/avatar/components/Avatar.module.scss.d.ts b/packages/twenty-ui/src/display/avatar/components/Avatar.module.scss.d.ts new file mode 100644 index 0000000000..52e8e87219 --- /dev/null +++ b/packages/twenty-ui/src/display/avatar/components/Avatar.module.scss.d.ts @@ -0,0 +1,11 @@ +declare const classNames: { + readonly root: 'root'; + readonly xl: 'xl'; + readonly lg: 'lg'; + readonly md: 'md'; + readonly sm: 'sm'; + readonly xs: 'xs'; + readonly image: 'image'; + readonly placeholderChar: 'placeholderChar'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/avatar/components/Avatar.tsx b/packages/twenty-ui/src/display/avatar/components/Avatar.tsx new file mode 100644 index 0000000000..56c3520478 --- /dev/null +++ b/packages/twenty-ui/src/display/avatar/components/Avatar.tsx @@ -0,0 +1,146 @@ +import { isNonEmptyString, isNull, isUndefined } from '@sniptt/guards'; +import { clsx } from 'clsx'; +import { useAtom } from 'jotai'; +import { useContext } from 'react'; + +import { invalidAvatarUrlsAtomV2 } from '@ui/display/avatar/components/states/invalidAvatarUrlsAtomV2'; +import { type AvatarSize } from '@ui/display/avatar/types/AvatarSize'; +import { type AvatarType } from '@ui/display/avatar/types/AvatarType'; +import { type IconComponent } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; +import { stringToThemeColorP3String } from '@ui/utilities'; +import { REACT_APP_SERVER_BASE_URL } from '@ui/utilities/config'; +import { type Nullable } from '@ui/utilities/types/Nullable'; +import { getImageAbsoluteURI } from '@ui/utilities/utils/getImageAbsoluteURI'; + +import styles from './Avatar.module.scss'; + +export type AvatarProps = { + avatarUrl?: string | null; + className?: string; + size?: AvatarSize; + placeholder: string | undefined; + placeholderColorSeed?: string; + Icon?: IconComponent; + iconColor?: string; + type?: Nullable; + color?: string; + backgroundColor?: string; + borderColor?: string; + onClick?: () => void; +}; + +export const Avatar = ({ + avatarUrl, + className, + size = 'md', + placeholder, + placeholderColorSeed = placeholder, + Icon, + iconColor, + onClick, + type = 'squared', + color, + backgroundColor, + borderColor, +}: AvatarProps) => { + const { theme } = useContext(ThemeContext); + + const [invalidAvatarUrls, setInvalidAvatarUrls] = useAtom( + invalidAvatarUrlsAtomV2, + ); + + const avatarImageURI = isNonEmptyString(avatarUrl) + ? getImageAbsoluteURI({ + imageUrl: avatarUrl, + baseUrl: REACT_APP_SERVER_BASE_URL, + }) + : null; + + const placeholderFirstChar = placeholder?.trim()?.charAt(0); + const isPlaceholderFirstCharEmpty = + !placeholderFirstChar || placeholderFirstChar === ''; + const placeholderChar = placeholderFirstChar?.toUpperCase() || '-'; + + const showPlaceholder = + isNull(avatarImageURI) || invalidAvatarUrls.includes(avatarImageURI); + + const handleImageError = () => { + if (isNonEmptyString(avatarImageURI)) { + setInvalidAvatarUrls((prev) => [...prev, avatarImageURI]); + } + }; + + const fixedColor = isPlaceholderFirstCharEmpty + ? theme.font.color.tertiary + : (color ?? + stringToThemeColorP3String({ + string: placeholderColorSeed ?? '', + variant: 12, + theme, + })); + const fixedBackgroundColor = isPlaceholderFirstCharEmpty + ? theme.background.transparent.light + : (backgroundColor ?? + stringToThemeColorP3String({ + string: placeholderColorSeed ?? '', + variant: type === 'app' ? 5 : 4, + theme, + })); + + const fixedBorderColor = + type === 'app' + ? (borderColor ?? + (isPlaceholderFirstCharEmpty + ? undefined + : stringToThemeColorP3String({ + string: placeholderColorSeed ?? '', + variant: 6, + theme, + }))) + : undefined; + + const showBackgroundColor = showPlaceholder; + + const showBorderColor = showPlaceholder; + + const appliedBorderColor = showBorderColor ? fixedBorderColor : undefined; + + const avatarStyle = { + '--avatar-color': fixedColor, + '--avatar-background': Icon + ? 'inherit' + : showBackgroundColor + ? fixedBackgroundColor + : 'none', + ...(type === 'app' && appliedBorderColor + ? { '--avatar-border': `1px solid ${appliedBorderColor}` } + : {}), + } as React.CSSProperties; + + return ( +
+ {Icon ? ( + + ) : showPlaceholder ? ( + {placeholderChar} + ) : ( + + )} +
+ ); +}; diff --git a/packages/twenty-ui/src/display/avatar/components/AvatarGroup.module.scss b/packages/twenty-ui/src/display/avatar/components/AvatarGroup.module.scss new file mode 100644 index 0000000000..68c8c0de25 --- /dev/null +++ b/packages/twenty-ui/src/display/avatar/components/AvatarGroup.module.scss @@ -0,0 +1,12 @@ +.container { + align-items: center; + display: flex; +} + +.itemContainer { + margin-right: -3px; + + &:last-child { + margin-right: 0; + } +} diff --git a/packages/twenty-ui/src/display/avatar/components/AvatarGroup.module.scss.d.ts b/packages/twenty-ui/src/display/avatar/components/AvatarGroup.module.scss.d.ts new file mode 100644 index 0000000000..605772e081 --- /dev/null +++ b/packages/twenty-ui/src/display/avatar/components/AvatarGroup.module.scss.d.ts @@ -0,0 +1,5 @@ +declare const classNames: { + readonly container: 'container'; + readonly itemContainer: 'itemContainer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/avatar/components/AvatarGroup.tsx b/packages/twenty-ui/src/display/avatar/components/AvatarGroup.tsx new file mode 100644 index 0000000000..0b38fd19ed --- /dev/null +++ b/packages/twenty-ui/src/display/avatar/components/AvatarGroup.tsx @@ -0,0 +1,23 @@ +import { type ReactNode } from 'react'; + +import styles from './AvatarGroup.module.scss'; + +export type AvatarGroupProps = { + avatars: ReactNode[]; +}; + +const MAX_AVATARS_NB = 4; + +export const AvatarGroup = ({ avatars }: AvatarGroupProps) => { + if (!avatars.length) return null; + + return ( +
+ {avatars.slice(0, MAX_AVATARS_NB).map((avatar, index) => ( +
+ {avatar} +
+ ))} +
+ ); +}; diff --git a/packages/twenty-ui/src/display/avatar/components/__stories__/Avatar.stories.tsx b/packages/twenty-ui/src/display/avatar/components/__stories__/Avatar.stories.tsx new file mode 100644 index 0000000000..2d3146926e --- /dev/null +++ b/packages/twenty-ui/src/display/avatar/components/__stories__/Avatar.stories.tsx @@ -0,0 +1,50 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { + AVATAR_URL_MOCK, + ComponentDecorator, + JotaiRootDecorator, +} from '@ui/testing'; + +import { Avatar } from '../Avatar'; + +const meta: Meta = { + title: 'Modules/Users/Avatar', + component: Avatar, + decorators: [ComponentDecorator, JotaiRootDecorator], + args: { + avatarUrl: AVATAR_URL_MOCK, + size: 'md', + placeholder: 'E', + type: 'rounded', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Rounded: Story = {}; + +export const Squared: Story = { + args: { type: 'squared' }, +}; + +export const NoAvatarPictureRounded: Story = { + args: { avatarUrl: '' }, +}; + +export const NoAvatarPictureSquared: Story = { + args: { + ...NoAvatarPictureRounded.args, + ...Squared.args, + }, +}; + +export const App: Story = { + args: { + type: 'app', + avatarUrl: '', + placeholder: 'Acme', + placeholderColorSeed: 'acme-app', + }, +}; diff --git a/packages/twenty-ui/src/display/avatar/components/__stories__/AvatarGroup.stories.tsx b/packages/twenty-ui/src/display/avatar/components/__stories__/AvatarGroup.stories.tsx new file mode 100644 index 0000000000..6d8833f83b --- /dev/null +++ b/packages/twenty-ui/src/display/avatar/components/__stories__/AvatarGroup.stories.tsx @@ -0,0 +1,70 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { Avatar, type AvatarProps } from '@ui/display/avatar/components/Avatar'; +import { type AvatarSize } from '@ui/display/avatar/types/AvatarSize'; +import { type AvatarType } from '@ui/display/avatar/types/AvatarType'; +import { + AVATAR_URL_MOCK, + CatalogDecorator, + ComponentDecorator, + JotaiRootDecorator, +} from '@ui/testing'; + +import { AvatarGroup, type AvatarGroupProps } from '../AvatarGroup'; + +const makeAvatar = (userName: string, props: Partial = {}) => ( + // oxlint-disable-next-line react/jsx-props-no-spreading + +); + +const getAvatars = (commonProps: Partial = {}) => [ + makeAvatar('Matthew', { avatarUrl: AVATAR_URL_MOCK, ...commonProps }), + makeAvatar('Sophie', commonProps), + makeAvatar('Jane', commonProps), + makeAvatar('Lily', commonProps), + makeAvatar('John', commonProps), +]; + +const meta: Meta< + AvatarGroupProps & AvatarProps & { numberOfAvatars?: number } +> = { + title: 'Modules/Users/AvatarGroup', + component: AvatarGroup, + render: ({ numberOfAvatars = 5, ...args }) => ( + + ), +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + decorators: [ComponentDecorator, JotaiRootDecorator], +}; + +export const Catalog: Story = { + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + catalog: { + dimensions: [ + { + name: 'number of avatars', + values: [1, 2, 3, 4, 5], + props: (numberOfAvatars: number) => ({ numberOfAvatars }), + }, + { + name: 'types', + values: ['rounded', 'squared'] as AvatarType[], + props: (type: AvatarType) => ({ type }), + }, + { + name: 'sizes', + values: ['xs', 'sm', 'md', 'lg', 'xl'] as AvatarSize[], + props: (size: AvatarSize) => ({ size }), + }, + ], + }, + }, + decorators: [CatalogDecorator, JotaiRootDecorator], +}; diff --git a/packages/twenty-ui/src/display/avatar/components/states/invalidAvatarUrlsAtomV2.ts b/packages/twenty-ui/src/display/avatar/components/states/invalidAvatarUrlsAtomV2.ts new file mode 100644 index 0000000000..94ae8d03b3 --- /dev/null +++ b/packages/twenty-ui/src/display/avatar/components/states/invalidAvatarUrlsAtomV2.ts @@ -0,0 +1,4 @@ +import { atom } from 'jotai'; + +export const invalidAvatarUrlsAtomV2 = atom([]); +invalidAvatarUrlsAtomV2.debugLabel = 'invalidAvatarUrlsAtomV2'; diff --git a/packages/twenty-ui/src/display/avatar/constants/AvatarPropertiesBySize.ts b/packages/twenty-ui/src/display/avatar/constants/AvatarPropertiesBySize.ts new file mode 100644 index 0000000000..e44c170cc2 --- /dev/null +++ b/packages/twenty-ui/src/display/avatar/constants/AvatarPropertiesBySize.ts @@ -0,0 +1,22 @@ +export const AVATAR_PROPERTIES_BY_SIZE = { + xl: { + fontSize: '16px', + width: '40px', + }, + lg: { + fontSize: '13px', + width: '24px', + }, + md: { + fontSize: '12px', + width: '16px', + }, + sm: { + fontSize: '10px', + width: '14px', + }, + xs: { + fontSize: '8px', + width: '12px', + }, +}; diff --git a/packages/twenty-ui/src/display/avatar/types/AvatarSize.ts b/packages/twenty-ui/src/display/avatar/types/AvatarSize.ts new file mode 100644 index 0000000000..db1915e171 --- /dev/null +++ b/packages/twenty-ui/src/display/avatar/types/AvatarSize.ts @@ -0,0 +1 @@ +export type AvatarSize = 'xl' | 'lg' | 'md' | 'sm' | 'xs'; diff --git a/packages/twenty-ui/src/display/avatar/types/AvatarType.ts b/packages/twenty-ui/src/display/avatar/types/AvatarType.ts new file mode 100644 index 0000000000..90bf7e685f --- /dev/null +++ b/packages/twenty-ui/src/display/avatar/types/AvatarType.ts @@ -0,0 +1 @@ +export type AvatarType = 'squared' | 'rounded' | 'icon' | 'app'; diff --git a/packages/twenty-ui/src/display/banner/components/Banner.module.scss b/packages/twenty-ui/src/display/banner/components/Banner.module.scss new file mode 100644 index 0000000000..944e358b53 --- /dev/null +++ b/packages/twenty-ui/src/display/banner/components/Banner.module.scss @@ -0,0 +1,39 @@ +.banner { + align-items: center; + box-sizing: border-box; + display: flex; + font-family: Inter; + font-size: var(--t-font-size-md); + font-style: normal; + font-weight: var(--t-font-weight-medium); + gap: var(--t-spacing-3); + justify-content: center; + line-height: 150%; + min-height: 40px; + padding: var(--t-spacing-2); + width: 100%; +} + +.primaryBlue { + backdrop-filter: blur(5px); + background: var(--t-color-blue); + color: var(--t-font-color-inverted); +} + +.primaryDanger { + backdrop-filter: blur(5px); + background: var(--t-color-red); + color: var(--t-font-color-inverted); +} + +.secondaryBlue { + backdrop-filter: none; + background: var(--t-background-transparent-blue); + color: var(--t-color-blue); +} + +.secondaryDanger { + backdrop-filter: none; + background: var(--t-background-transparent-danger); + color: var(--t-color-red); +} diff --git a/packages/twenty-ui/src/display/banner/components/Banner.module.scss.d.ts b/packages/twenty-ui/src/display/banner/components/Banner.module.scss.d.ts new file mode 100644 index 0000000000..d8caafc220 --- /dev/null +++ b/packages/twenty-ui/src/display/banner/components/Banner.module.scss.d.ts @@ -0,0 +1,8 @@ +declare const classNames: { + readonly banner: 'banner'; + readonly primaryBlue: 'primaryBlue'; + readonly primaryDanger: 'primaryDanger'; + readonly secondaryBlue: 'secondaryBlue'; + readonly secondaryDanger: 'secondaryDanger'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/banner/components/Banner.tsx b/packages/twenty-ui/src/display/banner/components/Banner.tsx new file mode 100644 index 0000000000..53d9dcfd65 --- /dev/null +++ b/packages/twenty-ui/src/display/banner/components/Banner.tsx @@ -0,0 +1,44 @@ +import { clsx } from 'clsx'; + +import styles from './Banner.module.scss'; + +export type BannerColor = 'blue' | 'danger'; + +export type BannerVariant = 'primary' | 'secondary'; + +const BANNER_VARIANT_COLOR_CLASS_NAMES = { + primary: { + blue: styles.primaryBlue, + danger: styles.primaryDanger, + }, + secondary: { + blue: styles.secondaryBlue, + danger: styles.secondaryDanger, + }, +} as const; + +type BannerProps = { + color?: BannerColor; + variant?: BannerVariant; + className?: string; + children: React.ReactNode; +}; + +export const Banner = ({ + color = 'blue', + variant = 'primary', + className, + children, +}: BannerProps) => { + return ( +
+ {children} +
+ ); +}; diff --git a/packages/twenty-ui/src/display/banner/components/InlineBanner.module.scss b/packages/twenty-ui/src/display/banner/components/InlineBanner.module.scss new file mode 100644 index 0000000000..23dbcdd0df --- /dev/null +++ b/packages/twenty-ui/src/display/banner/components/InlineBanner.module.scss @@ -0,0 +1,15 @@ +.banner { + border-radius: var(--t-border-radius-md); + margin-bottom: var(--t-spacing-4); +} + +.bannerContent { + align-items: center; + display: flex; + flex: 1; + gap: var(--t-spacing-2); +} + +.bannerText { + flex: 1; +} diff --git a/packages/twenty-ui/src/display/banner/components/InlineBanner.module.scss.d.ts b/packages/twenty-ui/src/display/banner/components/InlineBanner.module.scss.d.ts new file mode 100644 index 0000000000..c3c16a1e1b --- /dev/null +++ b/packages/twenty-ui/src/display/banner/components/InlineBanner.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly banner: 'banner'; + readonly bannerContent: 'bannerContent'; + readonly bannerText: 'bannerText'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/banner/components/InlineBanner.tsx b/packages/twenty-ui/src/display/banner/components/InlineBanner.tsx new file mode 100644 index 0000000000..9044dee3a1 --- /dev/null +++ b/packages/twenty-ui/src/display/banner/components/InlineBanner.tsx @@ -0,0 +1,53 @@ +import { clsx } from 'clsx'; +import { useContext } from 'react'; +import { ThemeContext } from '@ui/theme-constants'; +import { Button } from '@ui/input/button/components/Button/Button'; +import { type IconComponent } from '@ui/display/icon/types/IconComponent'; +import { IconInfoCircle } from '@ui/display/icon/components/TablerIcons'; +import { Banner, type BannerColor } from './Banner'; + +import styles from './InlineBanner.module.scss'; + +type InlineBannerProps = { + color?: BannerColor; + message: string; + button?: { + title?: string; + onClick?: () => void; + hidden?: boolean; + }; + LeftIcon?: IconComponent; + className?: string; +}; + +export const InlineBanner = ({ + color, + message, + button, + LeftIcon = IconInfoCircle, + className, +}: InlineBannerProps) => { + const { theme } = useContext(ThemeContext); + + return ( + +
+ + {message} +
+ {button && !button.hidden && ( +
+ + + + ), + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: {}, + argTypes: { + color: { control: false }, + variant: { control: false }, + }, + render: (args) => ( + // oxlint-disable-next-line react/jsx-props-no-spreading + +
+ Sync lost with mailbox hello@twenty.com. Please reconnect for updates: +
+ +
+ ), + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + catalog: { + dimensions: [ + { + name: 'variant', + values: ['primary', 'secondary'] satisfies BannerVariant[], + props: (variant: BannerVariant) => ({ variant }), + }, + { + name: 'color', + values: ['blue', 'danger'] satisfies BannerColor[], + props: (color: BannerColor) => ({ color }), + }, + ], + options: { + elementContainer: { + width: 700, + }, + }, + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/display/banner/components/__stories__/SidePanelInformationBanner.stories.tsx b/packages/twenty-ui/src/display/banner/components/__stories__/SidePanelInformationBanner.stories.tsx new file mode 100644 index 0000000000..c67b6be985 --- /dev/null +++ b/packages/twenty-ui/src/display/banner/components/__stories__/SidePanelInformationBanner.stories.tsx @@ -0,0 +1,37 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { ComponentDecorator } from '@ui/testing'; +import { SidePanelInformationBanner } from '../SidePanelInformationBanner'; + +const meta: Meta = { + title: 'UI/Display/Banner/SidePanelInformationBanner', + component: SidePanelInformationBanner, + decorators: [ComponentDecorator], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + message: 'Max 200 bars per chart. Consider adding a filter', + }, +}; + +export const ShortMessage: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + message: 'Too many groups', + }, +}; + +export const LongMessage: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + message: + 'The chart has exceeded the maximum number of 200 bars. Please consider adding filters to reduce the number of data points displayed.', + }, +}; diff --git a/packages/twenty-ui/src/display/callout/Callout.module.scss b/packages/twenty-ui/src/display/callout/Callout.module.scss new file mode 100644 index 0000000000..db337e72c7 --- /dev/null +++ b/packages/twenty-ui/src/display/callout/Callout.module.scss @@ -0,0 +1,115 @@ +.container { + align-items: flex-start; + border: 1px solid; + border-radius: var(--t-border-radius-md); + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: var(--t-spacing-2); + max-width: 512px; + padding: var(--t-spacing-3) var(--t-spacing-3) var(--t-spacing-2); + width: 100%; +} + +.containerInfo { + background-color: var(--t-accent-accent1); + border-color: var(--t-accent-accent6); +} + +.containerWarning { + background-color: var(--t-color-orange1); + border-color: var(--t-color-orange6); +} + +.containerSuccess { + background-color: var(--t-color-turquoise1); + border-color: var(--t-color-turquoise6); +} + +.containerError { + background-color: var(--t-color-red1); + border-color: var(--t-color-red6); +} + +.containerNeutral { + background-color: var(--t-color-gray1); + border-color: var(--t-color-gray6); +} + +.header { + align-items: center; + align-self: stretch; + display: flex; + flex-direction: row; + gap: var(--t-spacing-2); + min-height: var(--t-spacing-6); +} + +.iconContainer { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + height: var(--t-spacing-4); + width: var(--t-spacing-4); +} + +.iconContainerInfo { + color: var(--t-accent-accent9); +} + +.iconContainerWarning { + color: var(--t-color-orange9); +} + +.iconContainerSuccess { + color: var(--t-color-turquoise9); +} + +.iconContainerError { + color: var(--t-color-red9); +} + +.iconContainerNeutral { + color: var(--t-color-gray9); +} + +.title { + flex: 1; + color: var(--t-font-color-primary); + font-family: var(--t-font-family); + font-size: var(--t-font-size-md); + font-weight: var(--t-font-weight-medium); + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.descriptionWrapper { + align-items: center; + display: flex; + align-self: stretch; + padding-bottom: var(--t-spacing-2); + padding-left: var(--t-spacing-6); +} + +.descriptionWrapperWithAction { + padding-bottom: 0; +} + +.description { + flex: 1; + color: var(--t-font-color-tertiary); + font-family: var(--t-font-family); + font-size: var(--t-font-size-sm); + font-weight: var(--t-font-weight-regular); + line-height: 1.4; +} + +.footer { + align-items: center; + align-self: stretch; + display: flex; + justify-content: flex-end; +} diff --git a/packages/twenty-ui/src/display/callout/Callout.module.scss.d.ts b/packages/twenty-ui/src/display/callout/Callout.module.scss.d.ts new file mode 100644 index 0000000000..c55e1c0f9d --- /dev/null +++ b/packages/twenty-ui/src/display/callout/Callout.module.scss.d.ts @@ -0,0 +1,21 @@ +declare const classNames: { + readonly container: 'container'; + readonly containerInfo: 'containerInfo'; + readonly containerWarning: 'containerWarning'; + readonly containerSuccess: 'containerSuccess'; + readonly containerError: 'containerError'; + readonly containerNeutral: 'containerNeutral'; + readonly header: 'header'; + readonly iconContainer: 'iconContainer'; + readonly iconContainerInfo: 'iconContainerInfo'; + readonly iconContainerWarning: 'iconContainerWarning'; + readonly iconContainerSuccess: 'iconContainerSuccess'; + readonly iconContainerError: 'iconContainerError'; + readonly iconContainerNeutral: 'iconContainerNeutral'; + readonly title: 'title'; + readonly descriptionWrapper: 'descriptionWrapper'; + readonly descriptionWrapperWithAction: 'descriptionWrapperWithAction'; + readonly description: 'description'; + readonly footer: 'footer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/callout/Callout.tsx b/packages/twenty-ui/src/display/callout/Callout.tsx new file mode 100644 index 0000000000..1873615904 --- /dev/null +++ b/packages/twenty-ui/src/display/callout/Callout.tsx @@ -0,0 +1,116 @@ +import { clsx } from 'clsx'; +import { useState } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +import { IconHelp, IconX } from '@ui/display/icon/components/TablerIcons'; +import { type IconComponent } from '@ui/display/icon/types/IconComponent'; +import { LightButton, LightIconButton } from '@ui/input'; + +import styles from './Callout.module.scss'; + +export type CalloutVariant = + | 'info' + | 'warning' + | 'error' + | 'neutral' + | 'success'; + +const CALLOUT_CONTAINER_VARIANT_CLASS_NAMES: Record = { + info: styles.containerInfo, + warning: styles.containerWarning, + error: styles.containerError, + neutral: styles.containerNeutral, + success: styles.containerSuccess, +}; + +const CALLOUT_ICON_VARIANT_CLASS_NAMES: Record = { + info: styles.iconContainerInfo, + warning: styles.iconContainerWarning, + error: styles.iconContainerError, + neutral: styles.iconContainerNeutral, + success: styles.iconContainerSuccess, +}; + +export type CalloutProps = { + variant: CalloutVariant; + title: string; + description: string; + Icon?: IconComponent; + action?: { + label: string; + onClick: () => void; + }; + isClosable?: boolean; + onClose?: () => void; +}; + +export const Callout = ({ + variant, + title, + description, + Icon = IconHelp, + action, + isClosable = false, + onClose, +}: CalloutProps) => { + const [isVisible, setIsVisible] = useState(true); + + const handleClose = () => { + if (!isClosable) { + return; + } + + setIsVisible(false); + onClose?.(); + }; + + if (!isVisible) { + return null; + } + + return ( +
+
+
+ +
+
{title}
+ {isClosable && ( + + )} +
+
+
{description}
+
+ {isDefined(action) && ( +
+ +
+ )} +
+ ); +}; diff --git a/packages/twenty-ui/src/display/callout/__stories__/Callout.stories.tsx b/packages/twenty-ui/src/display/callout/__stories__/Callout.stories.tsx new file mode 100644 index 0000000000..48a232907c --- /dev/null +++ b/packages/twenty-ui/src/display/callout/__stories__/Callout.stories.tsx @@ -0,0 +1,73 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { + CatalogDecorator, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; + +import { Callout } from '@ui/display'; +import { type CalloutVariant } from '@ui/display/callout/Callout'; + +const meta: Meta = { + title: 'UI/Display/Callout', + component: Callout, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + variant: 'neutral', + title: 'This form will appear in workflow runs.', + description: + 'Because this workflow is not using a manual trigger, the form will not open on top of the interface. To fill it, open the corresponding workflow run and complete the form there.', + action: { + label: 'Learn more', + onClick: () => {}, + }, + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: { + title: 'This form will appear in workflow runs.', + description: + 'Because this workflow is not using a manual trigger, the form will not open on top of the interface. To fill it, open the corresponding workflow run and complete the form there.', + action: { + label: 'Learn more', + onClick: () => {}, + }, + }, + argTypes: { + variant: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + catalog: { + dimensions: [ + { + name: 'accents', + values: [ + 'success', + 'warning', + 'error', + 'neutral', + 'info', + ] satisfies CalloutVariant[], + props: (variant: CalloutVariant) => ({ variant }), + }, + ], + options: { + elementContainer: { + width: 512, + }, + }, + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/display/checkmark/components/AnimatedCheckmark.tsx b/packages/twenty-ui/src/display/checkmark/components/AnimatedCheckmark.tsx new file mode 100644 index 0000000000..4a1faa110c --- /dev/null +++ b/packages/twenty-ui/src/display/checkmark/components/AnimatedCheckmark.tsx @@ -0,0 +1,42 @@ +import { motion } from 'framer-motion'; +import { useContext } from 'react'; +import { ThemeContext } from '@ui/theme-constants'; + +export type AnimatedCheckmarkProps = React.ComponentProps< + typeof motion.path +> & { + isAnimating?: boolean; + color?: string; + duration?: number; + size?: number; +}; + +export const AnimatedCheckmark = ({ + isAnimating = false, + color, + duration = 0.5, + size = 28, +}: AnimatedCheckmarkProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/checkmark/components/Checkmark.module.scss b/packages/twenty-ui/src/display/checkmark/components/Checkmark.module.scss new file mode 100644 index 0000000000..d254d14cd4 --- /dev/null +++ b/packages/twenty-ui/src/display/checkmark/components/Checkmark.module.scss @@ -0,0 +1,9 @@ +.root { + display: flex; + align-items: center; + justify-content: center; + width: var(--t-spacing-5); + height: var(--t-spacing-5); + border-radius: var(--t-border-radius-rounded); + background-color: var(--t-color-blue); +} diff --git a/packages/twenty-ui/src/display/checkmark/components/Checkmark.module.scss.d.ts b/packages/twenty-ui/src/display/checkmark/components/Checkmark.module.scss.d.ts new file mode 100644 index 0000000000..4d6743c3dc --- /dev/null +++ b/packages/twenty-ui/src/display/checkmark/components/Checkmark.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly root: 'root'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/checkmark/components/Checkmark.tsx b/packages/twenty-ui/src/display/checkmark/components/Checkmark.tsx new file mode 100644 index 0000000000..abd3d75a73 --- /dev/null +++ b/packages/twenty-ui/src/display/checkmark/components/Checkmark.tsx @@ -0,0 +1,22 @@ +import React, { useContext } from 'react'; + +import { clsx } from 'clsx'; + +import { IconCheck } from '@ui/display/icon/components/TablerIcons'; +import { ThemeContext } from '@ui/theme-constants'; + +import styles from './Checkmark.module.scss'; + +export type CheckmarkProps = React.ComponentPropsWithoutRef<'div'> & { + className?: string; +}; + +export const Checkmark = ({ className }: CheckmarkProps) => { + const { theme } = useContext(ThemeContext); + + return ( +
+ +
+ ); +}; diff --git a/packages/twenty-ui/src/display/checkmark/components/__stories__/Checkmark.stories.tsx b/packages/twenty-ui/src/display/checkmark/components/__stories__/Checkmark.stories.tsx new file mode 100644 index 0000000000..5ca4350241 --- /dev/null +++ b/packages/twenty-ui/src/display/checkmark/components/__stories__/Checkmark.stories.tsx @@ -0,0 +1,20 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { ComponentDecorator } from '@ui/testing/decorators/ComponentDecorator'; + +import { Checkmark } from '../Checkmark'; + +const meta: Meta = { + title: 'UI/Display/Checkmark', + component: Checkmark, + decorators: [ComponentDecorator], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { args: {} }; + +export const WithCustomStyles: Story = { + args: { style: { backgroundColor: 'red', height: 40, width: 40 } }, +}; diff --git a/packages/twenty-ui/src/display/color/components/ColorSample.module.scss b/packages/twenty-ui/src/display/color/components/ColorSample.module.scss new file mode 100644 index 0000000000..70b3b1725a --- /dev/null +++ b/packages/twenty-ui/src/display/color/components/ColorSample.module.scss @@ -0,0 +1,31 @@ +.root { + box-sizing: content-box; + display: block; + flex-shrink: 0; + width: var(--t-spacing-3); + height: var(--t-spacing-4); + border: 1px solid var(--color-sample-border-color); + border-radius: 60px; + background-color: var(--color-sample-color); +} + +.circle { + width: var(--t-spacing-4); + border-radius: 50%; +} + +.pipeline { + display: flex; + align-items: center; + justify-content: center; + border: 0; + + &::after { + display: block; + width: var(--t-spacing-1); + height: var(--t-spacing-1); + border-radius: var(--t-border-radius-rounded); + background-color: var(--color-sample-color); + content: ''; + } +} diff --git a/packages/twenty-ui/src/display/color/components/ColorSample.module.scss.d.ts b/packages/twenty-ui/src/display/color/components/ColorSample.module.scss.d.ts new file mode 100644 index 0000000000..868ad8c17d --- /dev/null +++ b/packages/twenty-ui/src/display/color/components/ColorSample.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly root: 'root'; + readonly circle: 'circle'; + readonly pipeline: 'pipeline'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/color/components/ColorSample.tsx b/packages/twenty-ui/src/display/color/components/ColorSample.tsx new file mode 100644 index 0000000000..763c3db47e --- /dev/null +++ b/packages/twenty-ui/src/display/color/components/ColorSample.tsx @@ -0,0 +1,55 @@ +import { clsx } from 'clsx'; + +import { type ThemeColor } from '@ui/theme'; +import { themeCssVariables } from '@ui/theme-constants'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +import styles from './ColorSample.module.scss'; + +export type ColorSampleVariant = 'circle' | 'default' | 'pipeline'; + +type StyledColorSampleProps = { + colorName: ThemeColor; + color?: string; + variant?: ColorSampleVariant; +}; + +export type ColorSampleProps = StyledColorSampleProps & { + className?: string; +}; + +const getColor = (colorName: ThemeColor, color?: string) => { + if (isDefined(color)) { + return color; + } + + return themeCssVariables.tag.background[colorName]; +}; + +const getBorderColor = (colorName: ThemeColor) => { + return themeCssVariables.tag.text[colorName]; +}; + +export const ColorSample = ({ + colorName, + color, + variant, + className, +}: ColorSampleProps) => { + return ( +
+ ); +}; diff --git a/packages/twenty-ui/src/display/color/components/__stories__/ColorSample.stories.tsx b/packages/twenty-ui/src/display/color/components/__stories__/ColorSample.stories.tsx new file mode 100644 index 0000000000..22f966796b --- /dev/null +++ b/packages/twenty-ui/src/display/color/components/__stories__/ColorSample.stories.tsx @@ -0,0 +1,25 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { ComponentDecorator } from '@ui/testing'; + +import { ColorSample } from '../ColorSample'; + +const meta: Meta = { + title: 'UI/Display/Color/ColorSample', + component: ColorSample, + decorators: [ComponentDecorator], + args: { colorName: 'green' }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Pipeline: Story = { + args: { variant: 'pipeline' }, +}; + +export const Circle: Story = { + args: { variant: 'circle' }, +}; diff --git a/packages/twenty-ui/src/display/command-block/components/CommandBlock.module.scss b/packages/twenty-ui/src/display/command-block/components/CommandBlock.module.scss new file mode 100644 index 0000000000..d5012c943d --- /dev/null +++ b/packages/twenty-ui/src/display/command-block/components/CommandBlock.module.scss @@ -0,0 +1,26 @@ +.container { + border-radius: var(--t-border-radius-sm); + border: 1px solid var(--t-border-color-medium); + background: var(--t-background-secondary); + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--t-spacing-3); + gap: var(--t-spacing-3); +} + +.commandContain { + font-family: var(--t-code-font-family); +} + +.buttonContainer { + display: flex; +} + +.lineStartSpan { + color: var(--t-code-text-orange); +} + +.lineSpan { + color: var(--t-code-text-green); +} diff --git a/packages/twenty-ui/src/display/command-block/components/CommandBlock.module.scss.d.ts b/packages/twenty-ui/src/display/command-block/components/CommandBlock.module.scss.d.ts new file mode 100644 index 0000000000..155a1a2996 --- /dev/null +++ b/packages/twenty-ui/src/display/command-block/components/CommandBlock.module.scss.d.ts @@ -0,0 +1,8 @@ +declare const classNames: { + readonly container: 'container'; + readonly commandContain: 'commandContain'; + readonly buttonContainer: 'buttonContainer'; + readonly lineStartSpan: 'lineStartSpan'; + readonly lineSpan: 'lineSpan'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/command-block/components/CommandBlock.tsx b/packages/twenty-ui/src/display/command-block/components/CommandBlock.tsx new file mode 100644 index 0000000000..3de3f0df4b --- /dev/null +++ b/packages/twenty-ui/src/display/command-block/components/CommandBlock.tsx @@ -0,0 +1,26 @@ +import { type ReactElement } from 'react'; + +import styles from './CommandBlock.module.scss'; + +type CommandBlockProps = { + commands: string[]; + button?: ReactElement; +}; + +export const CommandBlock = ({ commands, button }: CommandBlockProps) => { + return ( +
+
+ <> + {commands.map((line, i) => ( +
+ {'> '} + {line} +
+ ))} + +
+ {button &&
{button}
} +
+ ); +}; diff --git a/packages/twenty-ui/src/display/components/LinkifiedText.module.scss b/packages/twenty-ui/src/display/components/LinkifiedText.module.scss new file mode 100644 index 0000000000..861a4f33c1 --- /dev/null +++ b/packages/twenty-ui/src/display/components/LinkifiedText.module.scss @@ -0,0 +1,8 @@ +.link { + color: var(--t-color-blue); + text-decoration: underline; + + &:hover { + text-decoration-color: var(--t-color-blue); + } +} diff --git a/packages/twenty-ui/src/display/components/LinkifiedText.module.scss.d.ts b/packages/twenty-ui/src/display/components/LinkifiedText.module.scss.d.ts new file mode 100644 index 0000000000..0361a30bb3 --- /dev/null +++ b/packages/twenty-ui/src/display/components/LinkifiedText.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly link: 'link'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/components/LinkifiedText.tsx b/packages/twenty-ui/src/display/components/LinkifiedText.tsx new file mode 100644 index 0000000000..fb074a7000 --- /dev/null +++ b/packages/twenty-ui/src/display/components/LinkifiedText.tsx @@ -0,0 +1,36 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +import { linkifyText } from '@ui/utilities/utils/linkifyText'; + +import styles from './LinkifiedText.module.scss'; + +type LinkifiedTextProps = { + text: string; +}; + +export const LinkifiedText = ({ text }: LinkifiedTextProps) => { + if (!isNonEmptyString(text)) { + return null; + } + + return ( + <> + {linkifyText(text).map((part, index) => + part.type === 'link' ? ( + e.stopPropagation()} + > + {part.content} + + ) : ( + part.content + ), + )} + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/Icon.tsx b/packages/twenty-ui/src/display/icon/components/Icon.tsx new file mode 100644 index 0000000000..a692a9dacf --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/Icon.tsx @@ -0,0 +1,29 @@ +import { useIcons } from '@ui/display/icon/hooks/useIcons'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; + +export type IconProps = IconComponentProps & { + name: string; +}; + +export const Icon = ({ + name, + className, + style, + size, + stroke, + color, +}: IconProps) => { + const { getIcon } = useIcons(); + + const ResolvedIcon = getIcon(name); + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconAddressBook.tsx b/packages/twenty-ui/src/display/icon/components/IconAddressBook.tsx new file mode 100644 index 0000000000..78759ab0a5 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconAddressBook.tsx @@ -0,0 +1,25 @@ +import { useContext } from 'react'; + +import IconAddressBookRaw from '@assets/icons/address-book.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconAddressBookProps = Pick< + IconComponentProps, + 'size' | 'stroke' | 'color' +>; + +export const IconAddressBook = (props: IconAddressBookProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? 24; + const stroke = props.stroke ?? theme.icon.stroke.md; + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconBrandAnthropic.tsx b/packages/twenty-ui/src/display/icon/components/IconBrandAnthropic.tsx new file mode 100644 index 0000000000..3e77d98502 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconBrandAnthropic.tsx @@ -0,0 +1,14 @@ +import { useContext } from 'react'; + +import IconAnthropicRaw from '@assets/icons/anthropic.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconBrandAnthropicProps = Pick; + +export const IconBrandAnthropic = (props: IconBrandAnthropicProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ; +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconBrandGemini.tsx b/packages/twenty-ui/src/display/icon/components/IconBrandGemini.tsx new file mode 100644 index 0000000000..80a7923c35 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconBrandGemini.tsx @@ -0,0 +1,14 @@ +import { useContext } from 'react'; + +import IconBrandGeminiRaw from '@assets/icons/gemini.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconBrandGeminiProps = Pick; + +export const IconBrandGemini = (props: IconBrandGeminiProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ; +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconBrandGroq.tsx b/packages/twenty-ui/src/display/icon/components/IconBrandGroq.tsx new file mode 100644 index 0000000000..abc50bd58d --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconBrandGroq.tsx @@ -0,0 +1,14 @@ +import { useContext } from 'react'; + +import IconBrandGroqRaw from '@assets/icons/groq.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconBrandGroqProps = Pick; + +export const IconBrandGroq = (props: IconBrandGroqProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ; +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconBrandMistral.tsx b/packages/twenty-ui/src/display/icon/components/IconBrandMistral.tsx new file mode 100644 index 0000000000..542c9eec1d --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconBrandMistral.tsx @@ -0,0 +1,14 @@ +import { useContext } from 'react'; + +import IconBrandMistralRaw from '@assets/icons/mistral.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconBrandMistralProps = Pick; + +export const IconBrandMistral = (props: IconBrandMistralProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ; +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconBrandXai.tsx b/packages/twenty-ui/src/display/icon/components/IconBrandXai.tsx new file mode 100644 index 0000000000..d0d1a7ab29 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconBrandXai.tsx @@ -0,0 +1,20 @@ +import { useContext } from 'react'; + +import IconBrandXaiRaw from '@assets/icons/xai.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconBrandXaiProps = Pick; + +export const IconBrandXai = (props: IconBrandXaiProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconChartBarHorizontal.module.scss b/packages/twenty-ui/src/display/icon/components/IconChartBarHorizontal.module.scss new file mode 100644 index 0000000000..fc0cf1ef70 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconChartBarHorizontal.module.scss @@ -0,0 +1,4 @@ +.rotatedIconWrapper { + display: inline-flex; + transform: rotate(90deg); +} diff --git a/packages/twenty-ui/src/display/icon/components/IconChartBarHorizontal.module.scss.d.ts b/packages/twenty-ui/src/display/icon/components/IconChartBarHorizontal.module.scss.d.ts new file mode 100644 index 0000000000..0f515955a1 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconChartBarHorizontal.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly rotatedIconWrapper: 'rotatedIconWrapper'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/icon/components/IconChartBarHorizontal.tsx b/packages/twenty-ui/src/display/icon/components/IconChartBarHorizontal.tsx new file mode 100644 index 0000000000..bab4d674bb --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconChartBarHorizontal.tsx @@ -0,0 +1,24 @@ +import { useContext } from 'react'; + +import { IconChartBar } from '@ui/display/icon/components/TablerIcons'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +import styles from './IconChartBarHorizontal.module.scss'; + +type IconChartBarHorizontalProps = Pick< + IconComponentProps, + 'size' | 'stroke' | 'color' +>; + +export const IconChartBarHorizontal = (props: IconChartBarHorizontalProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.md; + const stroke = props.stroke ?? theme.icon.stroke.sm; + + return ( +
+ +
+ ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconGmail.tsx b/packages/twenty-ui/src/display/icon/components/IconGmail.tsx new file mode 100644 index 0000000000..961fc5d688 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconGmail.tsx @@ -0,0 +1,14 @@ +import { useContext } from 'react'; + +import IconGmailRaw from '@assets/icons/gmail.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconGmailProps = Pick; + +export const IconGmail = (props: IconGmailProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ; +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconGoogle.tsx b/packages/twenty-ui/src/display/icon/components/IconGoogle.tsx new file mode 100644 index 0000000000..4ae7dac95e --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconGoogle.tsx @@ -0,0 +1,14 @@ +import { useContext } from 'react'; + +import IconGoogleRaw from '@assets/icons/google.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconGoogleProps = Pick; + +export const IconGoogle = (props: IconGoogleProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ; +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconGoogleCalendar.tsx b/packages/twenty-ui/src/display/icon/components/IconGoogleCalendar.tsx new file mode 100644 index 0000000000..1db26ddec9 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconGoogleCalendar.tsx @@ -0,0 +1,14 @@ +import { useContext } from 'react'; + +import IconGoogleCalendarRaw from '@assets/icons/google-calendar.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconGoogleCalendarProps = Pick; + +export const IconGoogleCalendar = (props: IconGoogleCalendarProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ; +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconLock.tsx b/packages/twenty-ui/src/display/icon/components/IconLock.tsx new file mode 100644 index 0000000000..3165280066 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconLock.tsx @@ -0,0 +1,14 @@ +import { useContext } from 'react'; + +import IconLockRaw from '@assets/icons/lock.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconLockCustomProps = Pick; + +export const IconLockCustom = (props: IconLockCustomProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ; +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconMicrosoft.tsx b/packages/twenty-ui/src/display/icon/components/IconMicrosoft.tsx new file mode 100644 index 0000000000..5bc803f493 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconMicrosoft.tsx @@ -0,0 +1,15 @@ +import { useContext } from 'react'; + +import IconMicrosoftRaw from '@assets/icons/microsoft.svg?react'; +import { ThemeContext } from '@ui/theme-constants'; + +interface IconMicrosoftProps { + size?: number | string; +} + +export const IconMicrosoft = (props: IconMicrosoftProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ; +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconMicrosoftCalendar.tsx b/packages/twenty-ui/src/display/icon/components/IconMicrosoftCalendar.tsx new file mode 100644 index 0000000000..894fbebfda --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconMicrosoftCalendar.tsx @@ -0,0 +1,15 @@ +import { useContext } from 'react'; + +import IconMicrosoftCalendarRaw from '@assets/icons/microsoft-calendar.svg?react'; +import { ThemeContext } from '@ui/theme-constants'; + +interface IconMicrosoftCalendarProps { + size?: number | string; +} + +export const IconMicrosoftCalendar = (props: IconMicrosoftCalendarProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ; +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconMicrosoftOutlook.tsx b/packages/twenty-ui/src/display/icon/components/IconMicrosoftOutlook.tsx new file mode 100644 index 0000000000..4e04adac43 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconMicrosoftOutlook.tsx @@ -0,0 +1,15 @@ +import { useContext } from 'react'; + +import IconMicrosoftOutlookRaw from '@assets/icons/microsoft-outlook.svg?react'; +import { ThemeContext } from '@ui/theme-constants'; + +interface IconMicrosoftOutlookProps { + size?: number | string; +} + +export const IconMicrosoftOutlook = (props: IconMicrosoftOutlookProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ; +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconModelClaude.tsx b/packages/twenty-ui/src/display/icon/components/IconModelClaude.tsx new file mode 100644 index 0000000000..fb2f64eb77 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconModelClaude.tsx @@ -0,0 +1,14 @@ +import { useContext } from 'react'; + +import IconModelClaudeRaw from '@assets/icons/claude.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconModelClaudeProps = Pick; + +export const IconModelClaude = (props: IconModelClaudeProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ; +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconProviderOpenai.tsx b/packages/twenty-ui/src/display/icon/components/IconProviderOpenai.tsx new file mode 100644 index 0000000000..ec64dc03ee --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconProviderOpenai.tsx @@ -0,0 +1,20 @@ +import { useContext } from 'react'; + +import IconOpenaiRaw from '@assets/icons/openai.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconProviderOpenaiProps = Pick; + +export const IconProviderOpenai = (props: IconProviderOpenaiProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconRelationManyToOne.tsx b/packages/twenty-ui/src/display/icon/components/IconRelationManyToOne.tsx new file mode 100644 index 0000000000..0f20794181 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconRelationManyToOne.tsx @@ -0,0 +1,17 @@ +import { useContext } from 'react'; + +import IconRelationManyToOneRaw from '@assets/icons/many-to-one.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconRelationManyToOneProps = Pick; + +export const IconRelationManyToOne = (props: IconRelationManyToOneProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? 24; + const stroke = props.stroke ?? theme.icon.stroke.md; + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconTrashXOff.tsx b/packages/twenty-ui/src/display/icon/components/IconTrashXOff.tsx new file mode 100644 index 0000000000..2d5aedfb5e --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconTrashXOff.tsx @@ -0,0 +1,15 @@ +import { useContext } from 'react'; + +import IconTrashXOffRaw from '@assets/icons/trash-x-off.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconTrashXOffProps = Pick; + +export const IconTrashXOff = (props: IconTrashXOffProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + const stroke = props.stroke ?? theme.icon.stroke.md; + + return ; +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconTwentyStar.tsx b/packages/twenty-ui/src/display/icon/components/IconTwentyStar.tsx new file mode 100644 index 0000000000..7309bb0c2c --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconTwentyStar.tsx @@ -0,0 +1,15 @@ +import { useContext } from 'react'; + +import IconTwentyStarRaw from '@assets/icons/twenty-star.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconTwentyStarProps = Pick; + +export const IconTwentyStar = (props: IconTwentyStarProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? 24; + const stroke = props.stroke ?? theme.icon.stroke.md; + + return ; +}; diff --git a/packages/twenty-ui/src/display/icon/components/IconTwentyStarFilled.tsx b/packages/twenty-ui/src/display/icon/components/IconTwentyStarFilled.tsx new file mode 100644 index 0000000000..051c98fffb --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconTwentyStarFilled.tsx @@ -0,0 +1,17 @@ +import { useContext } from 'react'; + +import IconTwentyStarFilledRaw from '@assets/icons/twenty-star-filled.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IconTwentyStarFilledProps = Pick; + +export const IconTwentyStarFilled = (props: IconTwentyStarFilledProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? 24; + const stroke = props.stroke ?? theme.icon.stroke.md; + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconArray.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconArray.tsx new file mode 100644 index 0000000000..17cf7c48a4 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconArray.tsx @@ -0,0 +1,23 @@ +import { useContext } from 'react'; + +import IllustrationIconArrayRaw from '@assets/icons/illustration-array.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconArrayProps = Pick; + +export const IllustrationIconArray = (props: IllustrationIconArrayProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconCalendarEvent.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconCalendarEvent.tsx new file mode 100644 index 0000000000..f622089925 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconCalendarEvent.tsx @@ -0,0 +1,25 @@ +import { useContext } from 'react'; + +import IllustrationIconCalendarEventRaw from '@assets/icons/illustration-calendar-event.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconCalendarEventProps = Pick; + +export const IllustrationIconCalendarEvent = ( + props: IllustrationIconCalendarEventProps, +) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconCalendarTime.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconCalendarTime.tsx new file mode 100644 index 0000000000..8027cb5048 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconCalendarTime.tsx @@ -0,0 +1,25 @@ +import { useContext } from 'react'; + +import IllustrationIconCalendarTimeRaw from '@assets/icons/illustration-calendar-time.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconCalendarTimeProps = Pick; + +export const IllustrationIconCalendarTime = ( + props: IllustrationIconCalendarTimeProps, +) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconCurrency.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconCurrency.tsx new file mode 100644 index 0000000000..0a0ef60343 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconCurrency.tsx @@ -0,0 +1,25 @@ +import { useContext } from 'react'; + +import IllustrationIconCurrencyRaw from '@assets/icons/illustration-currency.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconCurrencyProps = Pick; + +export const IllustrationIconCurrency = ( + props: IllustrationIconCurrencyProps, +) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconFile.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconFile.tsx new file mode 100644 index 0000000000..94be18214b --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconFile.tsx @@ -0,0 +1,23 @@ +import { useContext } from 'react'; + +import IllustrationIconFileRaw from '@assets/icons/illustration-file.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconFileProps = Pick; + +export const IllustrationIconFile = (props: IllustrationIconFileProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconJson.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconJson.tsx new file mode 100644 index 0000000000..c521ba4c8c --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconJson.tsx @@ -0,0 +1,23 @@ +import { useContext } from 'react'; + +import IllustrationIconJsonRaw from '@assets/icons/illustration-json.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconJsonProps = Pick; + +export const IllustrationIconJson = (props: IllustrationIconJsonProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconLink.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconLink.tsx new file mode 100644 index 0000000000..afaa9b1983 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconLink.tsx @@ -0,0 +1,23 @@ +import { useContext } from 'react'; + +import IllustrationIconLinkRaw from '@assets/icons/illustration-link.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconLinkProps = Pick; + +export const IllustrationIconLink = (props: IllustrationIconLinkProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconMail.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconMail.tsx new file mode 100644 index 0000000000..95f9ee133e --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconMail.tsx @@ -0,0 +1,23 @@ +import { useContext } from 'react'; + +import IllustrationIconMailRaw from '@assets/icons/illustration-mail.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconMailProps = Pick; + +export const IllustrationIconMail = (props: IllustrationIconMailProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconManyToMany.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconManyToMany.tsx new file mode 100644 index 0000000000..0fd8370f85 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconManyToMany.tsx @@ -0,0 +1,25 @@ +import { useContext } from 'react'; + +import IllustrationIconManyToManyRaw from '@assets/icons/illustration-many-to-many.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconManyToManyProps = Pick; + +export const IllustrationIconManyToMany = ( + props: IllustrationIconManyToManyProps, +) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconMap.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconMap.tsx new file mode 100644 index 0000000000..484ce4cd47 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconMap.tsx @@ -0,0 +1,23 @@ +import { useContext } from 'react'; + +import IllustrationIconMapRaw from '@assets/icons/illustration-map.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconMapProps = Pick; + +export const IllustrationIconMap = (props: IllustrationIconMapProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconNumbers.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconNumbers.tsx new file mode 100644 index 0000000000..0ac70a09fe --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconNumbers.tsx @@ -0,0 +1,25 @@ +import { useContext } from 'react'; + +import IllustrationIconNumbersRaw from '@assets/icons/illustration-numbers.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconNumbersProps = Pick; + +export const IllustrationIconNumbers = ( + props: IllustrationIconNumbersProps, +) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconOneToMany.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconOneToMany.tsx new file mode 100644 index 0000000000..7c8e50474f --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconOneToMany.tsx @@ -0,0 +1,25 @@ +import { useContext } from 'react'; + +import IllustrationIconOneToManyRaw from '@assets/icons/illustration-one-to-many.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconOneToManyProps = Pick; + +export const IllustrationIconOneToMany = ( + props: IllustrationIconOneToManyProps, +) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconOneToOne.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconOneToOne.tsx new file mode 100644 index 0000000000..54da40e2b2 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconOneToOne.tsx @@ -0,0 +1,25 @@ +import { useContext } from 'react'; + +import IllustrationIconOneToOneRaw from '@assets/icons/illustration-one-to-one.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconOneToOneProps = Pick; + +export const IllustrationIconOneToOne = ( + props: IllustrationIconOneToOneProps, +) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconPhone.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconPhone.tsx new file mode 100644 index 0000000000..e06371bb9a --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconPhone.tsx @@ -0,0 +1,24 @@ +import { useContext } from 'react'; + +import IllustrationIconPhoneRaw from '@assets/icons/illustration-phone.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconPhoneProps = Pick; + +export const IllustrationIconPhone = (props: IllustrationIconPhoneProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconSetting.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconSetting.tsx new file mode 100644 index 0000000000..10c96b0e6d --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconSetting.tsx @@ -0,0 +1,26 @@ +import { useContext } from 'react'; + +import IllustrationIconSettingRaw from '@assets/icons/illustration-setting.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconSettingProps = Pick; + +export const IllustrationIconSetting = ( + props: IllustrationIconSettingProps, +) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconStar.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconStar.tsx new file mode 100644 index 0000000000..e8c662c27f --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconStar.tsx @@ -0,0 +1,24 @@ +import { useContext } from 'react'; + +import IllustrationIconStarRaw from '@assets/icons/illustration-star.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconStarProps = Pick; + +export const IllustrationIconStar = (props: IllustrationIconStarProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconTag.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconTag.tsx new file mode 100644 index 0000000000..a45034ed09 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconTag.tsx @@ -0,0 +1,23 @@ +import { useContext } from 'react'; + +import IllustrationIconTagRaw from '@assets/icons/illustration-tag.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconTagProps = Pick; + +export const IllustrationIconTag = (props: IllustrationIconTagProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconTags.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconTags.tsx new file mode 100644 index 0000000000..667a812740 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconTags.tsx @@ -0,0 +1,23 @@ +import { useContext } from 'react'; + +import IllustrationIconTagsRaw from '@assets/icons/illustration-tags.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconTagsProps = Pick; + +export const IllustrationIconTags = (props: IllustrationIconTagsProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconText.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconText.tsx new file mode 100644 index 0000000000..9544e42ab6 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconText.tsx @@ -0,0 +1,23 @@ +import { useContext } from 'react'; + +import IllustrationIconTextRaw from '@assets/icons/illustration-text.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconTextProps = Pick; + +export const IllustrationIconText = (props: IllustrationIconTextProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconToggle.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconToggle.tsx new file mode 100644 index 0000000000..8dec6ec915 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconToggle.tsx @@ -0,0 +1,23 @@ +import { useContext } from 'react'; + +import IllustrationIconToggleRaw from '@assets/icons/illustration-toggle.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconToggleProps = Pick; + +export const IllustrationIconToggle = (props: IllustrationIconToggleProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconUid.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconUid.tsx new file mode 100644 index 0000000000..6c89066302 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconUid.tsx @@ -0,0 +1,23 @@ +import { useContext } from 'react'; + +import IllustrationIconUidRaw from '@assets/icons/illustration-uid.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconUidProps = Pick; + +export const IllustrationIconUid = (props: IllustrationIconUidProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconUser.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconUser.tsx new file mode 100644 index 0000000000..e76856f2a0 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconUser.tsx @@ -0,0 +1,23 @@ +import { useContext } from 'react'; + +import IllustrationIconUserRaw from '@assets/icons/illustration-user.svg?react'; +import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +type IllustrationIconUserProps = Pick; + +export const IllustrationIconUser = (props: IllustrationIconUserProps) => { + const { theme } = useContext(ThemeContext); + const size = props.size ?? theme.icon.size.lg; + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconWrapper.module.scss b/packages/twenty-ui/src/display/icon/components/IllustrationIconWrapper.module.scss new file mode 100644 index 0000000000..82edacd403 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconWrapper.module.scss @@ -0,0 +1,7 @@ +.root { + background-color: var(--t-background-primary); + border: 0.75px solid var(--t-border-color-medium); + border-radius: var(--t-border-radius-sm); + display: flex; + justify-content: center; +} diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconWrapper.module.scss.d.ts b/packages/twenty-ui/src/display/icon/components/IllustrationIconWrapper.module.scss.d.ts new file mode 100644 index 0000000000..4d6743c3dc --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconWrapper.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly root: 'root'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/icon/components/IllustrationIconWrapper.tsx b/packages/twenty-ui/src/display/icon/components/IllustrationIconWrapper.tsx new file mode 100644 index 0000000000..e75417a57c --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IllustrationIconWrapper.tsx @@ -0,0 +1,16 @@ +import { clsx } from 'clsx'; +import { forwardRef } from 'react'; + +import styles from './IllustrationIconWrapper.module.scss'; + +// The deprecated Linaria styled.div forwarded refs and accepted all native +// div props, so the port preserves that contract. +export const IllustrationIconWrapper = forwardRef< + HTMLDivElement, + React.ComponentPropsWithoutRef<'div'> +>(({ className, ...props }, ref) => ( + // oxlint-disable-next-line react/jsx-props-no-spreading +
+)); + +IllustrationIconWrapper.displayName = 'IllustrationIconWrapper'; diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts new file mode 100644 index 0000000000..cf92a1ad08 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -0,0 +1,435 @@ +/* oxlint-disable no-restricted-imports */ +export { + IconNumber123 as Icon123, + IconAdjustments, + IconAlertCircle, + IconAlertTriangle, + IconAlignBoxLeftTop, + IconAlignCenter, + IconAlignLeft, + IconAlignRight, + IconApi, + IconApps, + IconAppWindow, + IconArchive, + IconArchiveOff, + IconArrowBackUp, + IconArrowBarToDown, + IconArrowDown, + IconArrowLeft, + IconArrowMerge, + IconArrowRight, + IconArrowsDiagonal, + IconArrowsSort, + IconArrowsSplit2, + IconArrowsVertical, + IconArrowUp, + IconArrowUpRight, + IconAt, + IconAxisX, + IconAxisY, + IconBadge2k, + IconBaselineDensitySmall, + IconBell, + IconBlockquote, + IconBold, + IconBolt, + IconBook, + IconBook2, + IconBookmark, + IconBookmarkPlus, + IconBox, + IconBoxMultiple, + IconBrackets, + IconBracketsAngle, + IconBracketsContain, + IconBrain, + IconBrandDaysCounter, + IconBrandDocker, + IconBrandGithub, + IconBrandGoogle, + IconBrandGraphql, + IconBrandLinkedin, + IconBrandNpm, + IconBrandOpenai, + IconBrandX, + IconBriefcase, + IconBroadcast, + IconBrowserMaximize, + IconBuildingSkyscraper, + IconCalendar, + IconCalendarDue, + IconCalendarEvent, + IconCalendarMonth, + IconCalendarRepeat, + IconCalendarTime, + IconCalendarWeek, + IconCalendarX, + IconCancel, + IconCaretLeft, + IconCaretRight, + IconChartBar, + IconChartBarPopular, + IconChartCandle, + IconChartDots3, + IconChartLine, + IconChartPie, + IconCheck, + IconCheckbox, + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronRightPipe, + IconChevronsRight, + IconChevronUp, + IconCircle, + IconCircleDashed, + IconCircleDot, + IconCircleOff, + IconCirclePlus, + IconCircleX, + IconClick, + IconClock, + IconClockHour8, + IconClockPlay, + IconClockShare, + IconCode, + IconCodeCircle, + IconCoins, + IconColorSwatch, + IconColumnInsertRight, + IconColumns, + IconCommand, + IconMessageCircle as IconComment, + IconCopy, + IconCopyPlus, + IconCpu, + IconCreativeCommonsSa, + IconCreditCard, + IconCsv, + IconCube, + IconCurrencyAfghani, + IconCurrencyBahraini, + IconCurrencyBaht, + IconCurrencyDinar, + IconCurrencyDirham, + IconCurrencyDollar, + IconCurrencyDollarAustralian, + IconCurrencyDollarBrunei, + IconCurrencyDollarCanadian, + IconCurrencyDollarGuyanese, + IconCurrencyDollarSingapore, + IconCurrencyDong, + IconCurrencyDram, + IconCurrencyEuro, + IconCurrencyFlorin, + IconCurrencyForint, + IconCurrencyFrank, + IconCurrencyGuarani, + IconCurrencyHryvnia, + IconCurrencyIranianRial, + IconCurrencyKip, + IconCurrencyKroneCzech, + IconCurrencyKroneDanish, + IconCurrencyKroneSwedish, + IconCurrencyLari, + IconCurrencyLeu, + IconCurrencyLira, + IconCurrencyLyd, + IconCurrencyManat, + IconCurrencyNaira, + IconCurrencyPaanga, + IconCurrencyPeso, + IconCurrencyPound, + IconCurrencyQuetzal, + IconCurrencyReal, + IconCurrencyRenminbi, + IconCurrencyRiyal, + IconCurrencyRubel, + IconCurrencyRufiyaa, + IconCurrencyRupee, + IconCurrencyRupeeNepalese, + IconCurrencyShekel, + IconCurrencySom, + IconCurrencyTaka, + IconCurrencyTenge, + IconCurrencyTugrik, + IconCurrencyWon, + IconCurrencyYen, + IconCurrencyYuan, + IconCurrencyZloty, + IconDatabase, + IconDatabaseExport, + IconDecimal, + IconDeviceFloppy, + IconDoorEnter, + IconDotsVertical, + IconDownload, + IconEdit, + IconEditCircle, + IconEraser, + IconExclamationCircle, + IconExternalLink, + IconEye, + IconEyeOff, + IconEyeShare, + IconEyeX, + IconFile, + IconFileCheck, + IconFileExport, + IconFileImport, + IconFileInfo, + IconFilePencil, + IconFileText, + IconFileUpload, + IconFileZip, + IconFilter, + IconFilterCog, + IconFilterOff, + IconFilterPlus, + IconFilters, + IconFilterX, + IconFlag, + IconFlask, + IconFocusCentered, + IconFolder, + IconFolderOpen, + IconFolderPlus, + IconFolderRoot, + IconFolderSymlink, + IconForbid, + IconFrame, + IconFunction, + IconGauge, + IconGitBranch, + IconGitBranchDeleted, + IconGitCommit, + IconGizmo, + IconGraph, + IconGripVertical, + IconH1, + IconH2, + IconH3, + IconH4, + IconH5, + IconH6, + IconHandClick, + IconHandMove, + IconHeadphones, + IconHeart, + IconHeartOff, + IconHeartRateMonitor, + IconHelp, + IconHelpCircle, + IconHierarchy, + IconHierarchy2, + IconHistory, + IconHistoryToggle, + IconHome, + IconHourglassHigh, + IconHours24, + IconHttpDelete, + IconHttpGet, + IconHttpPatch, + IconHttpPost, + IconHttpPut, + IconId, + IconInbox, + IconInfoCircle, + IconItalic, + IconJetpack, + IconJson, + IconKey, + IconLanguage, + IconLayersLinked, + IconLayout, + IconLayoutDashboard, + IconLayoutGrid, + IconLayoutKanban, + IconLayoutList, + IconLayoutNavbar, + IconLayoutSidebarLeftCollapse, + IconLayoutSidebarLeftExpand, + IconLayoutSidebarRight, + IconLayoutSidebarRightCollapse, + IconLayoutSidebarRightExpand, + IconLego, + IconLetterK, + IconLibraryPlus, + IconLifebuoy, + IconLink, + IconLinkOff, + IconLinkPlus, + IconList, + IconListCheck, + IconListDetails, + IconListNumbers, + IconListSearch, + IconLoader, + IconLock, + IconLockOpen, + IconLogin2, + IconLogout, + IconMail, + IconMailCog, + IconMailX, + IconMap, + IconMathMax, + IconMathMin, + IconMathXy, + IconMaximize, + IconMessage, + IconMessageCirclePlus, + IconMinus, + IconMoneybag, + IconMoodSmile, + IconMoon, + IconMouse2, + IconNewSection, + IconNorthStar, + IconNoteOff, + IconNotes, + IconNumber, + IconNumber123, + IconNumber9, + IconNumbers, + IconPaint, + IconPaperclip, + IconPassword, + IconPencil, + IconPencilOff, + IconPercentage, + IconPhone, + IconPhoto, + IconPhotoUp, + IconPilcrow, + IconPin, + IconPinned, + IconPinnedOff, + IconPlayerPause, + IconPlayerPlay, + IconPlayerStop, + IconPlaylistAdd, + IconPlaystationSquare, + IconPlug, + IconPlus, + IconPoint, + IconPower, + IconPresentation, + IconPrinter, + IconProgressCheck, + IconPrompt, + IconPuzzle, + IconPuzzle2, + IconQuestionMark, + IconRadiusBottomLeft, + IconRadiusBottomRight, + IconRadiusTopLeft, + IconRadiusTopRight, + IconRefresh, + IconRefreshAlert, + IconRefreshDot, + IconRelationManyToMany, + IconRelationOneToMany, + IconRelationOneToOne, + IconReload, + IconReorder, + IconRepeat, + IconRestore, + IconRobot, + IconRocket, + IconRotate, + IconRotate2, + IconRowInsertBottom, + IconRowInsertTop, + IconSearch, + IconSend, + IconServer, + IconServer2, + IconSettings, + IconSettings2, + IconSettingsAutomation, + IconSettingsBolt, + IconShare, + IconShield, + IconSitemap, + IconSlash, + IconSortAscending, + IconSortAscendingLetters, + IconSortAscendingNumbers, + IconSortAZ, + IconSortDescending, + IconSortDescendingLetters, + IconSortDescendingNumbers, + IconSortZA, + IconSparkles, + IconSpy, + IconSql, + IconSquare, + IconSquareCheck, + IconSquareKey, + IconSquareNumber1, + IconSquareNumber2, + IconSquareNumber3, + IconSquareNumber4, + IconSquareNumber5, + IconSquareNumber6, + IconSquareNumber7, + IconSquareNumber8, + IconSquareNumber9, + IconSquarePlus, + IconSquareRoundedCheck, + IconSquareRoundedX, + IconSquareX, + IconStack2, + IconStar, + IconStatusChange, + IconStepInto, + IconStrikethrough, + IconSum, + IconSun, + IconSunMoon, + IconSwitchHorizontal, + IconTable, + IconTag, + IconTags, + IconTarget, + IconTargetArrow, + IconTerminal, + IconTestPipe, + IconTextCaption, + IconTextSize, + IconTextWrap, + IconTimeDuration30, + IconTimeDuration60, + IconTimelineEvent, + IconTool, + IconTransform, + IconTrash, + IconTrashOff, + IconTrashX, + IconTrendingDown, + IconTrendingUp, + IconTypography, + IconUnderline, + IconUnlink, + IconUpload, + IconUser, + IconUserCircle, + IconUserCog, + IconUserPin, + IconUserPlus, + IconUsers, + IconVariable, + IconVariablePlus, + IconVersions, + IconVersionsOff, + IconVideo, + IconWand, + IconWebhook, + IconWindow, + IconWorld, + IconX, +} from '@tabler/icons-react'; + +export type { IconProps as TablerIconsProps } from '@tabler/icons-react'; diff --git a/packages/twenty-ui/src/display/icon/components/ThinkingOrbitLoaderIcon.tsx b/packages/twenty-ui/src/display/icon/components/ThinkingOrbitLoaderIcon.tsx new file mode 100644 index 0000000000..af791e4b0b --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/ThinkingOrbitLoaderIcon.tsx @@ -0,0 +1,49 @@ +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; + +type ThinkingOrbitLoaderIconProps = Pick< + IconComponentProps, + 'className' | 'size' +>; + +export const ThinkingOrbitLoaderIcon = ({ + className, + size = 14, +}: ThinkingOrbitLoaderIconProps) => { + return ( + + ); +}; diff --git a/packages/twenty-ui/src/display/icon/components/__stories__/Icon.stories.tsx b/packages/twenty-ui/src/display/icon/components/__stories__/Icon.stories.tsx new file mode 100644 index 0000000000..91c92831b5 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/__stories__/Icon.stories.tsx @@ -0,0 +1,59 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { IconsProvider } from '@ui/display/icon/providers/IconsProvider'; +import { ComponentDecorator } from '@ui/testing/decorators/ComponentDecorator'; +import { JotaiRootDecorator } from '@ui/testing/decorators/JotaiRootDecorator'; + +import { Icon } from '../Icon'; + +const meta: Meta = { + title: 'UI/Display/Icon/Icon', + component: Icon, + decorators: [ + ComponentDecorator, + (Story) => ( + + + + ), + JotaiRootDecorator, + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + name: 'IconUser', + }, +}; + +export const WithCustomSize: Story = { + args: { + name: 'IconSettings', + size: 32, + }, +}; + +export const WithCustomColor: Story = { + args: { + name: 'IconHeart', + color: 'red', + size: 24, + }, +}; + +export const WithCustomStroke: Story = { + args: { + name: 'IconStar', + size: 24, + stroke: 1, + }, +}; + +export const InvalidName: Story = { + args: { + name: 'NonExistentIcon', + }, +}; diff --git a/packages/twenty-ui/src/display/icon/hooks/__tests__/useIcons.test.ts b/packages/twenty-ui/src/display/icon/hooks/__tests__/useIcons.test.ts new file mode 100644 index 0000000000..f12ce151fa --- /dev/null +++ b/packages/twenty-ui/src/display/icon/hooks/__tests__/useIcons.test.ts @@ -0,0 +1,47 @@ +import { renderHook } from '@testing-library/react'; +import { Provider } from 'jotai'; + +import { + Icon123, + IconBuildingSkyscraper, + IconUser, +} from '@ui/display/icon/components/TablerIcons'; +import { useIcons } from '@ui/display/icon/hooks/useIcons'; + +const mockedStateIcons = { + IconUser, + Icon123, + IconBuildingSkyscraper, +}; + +jest.mock('jotai', () => ({ + ...jest.requireActual('jotai'), + useAtomValue: () => mockedStateIcons, +})); + +describe('useIcons', () => { + const { result } = renderHook(() => useIcons(), { + wrapper: Provider, + }); + + it('returns default icon when no icon key is provided', () => { + expect(result.current.getIcon()).toEqual(Icon123); + }); + + it('returns the specified icon if the icon key exists in the iconsState', () => { + expect(result.current.getIcon('Icon123')).toEqual(Icon123); + expect(result.current.getIcon('IconUser')).toEqual(IconUser); + expect(result.current.getIcon('IconBuildingSkyscraper')).toEqual( + IconBuildingSkyscraper, + ); + }); + + it('returns default icon if the specified icon key does not exist in the iconsState', () => { + expect(result.current.getIcon('nonExistentKey')).toEqual(Icon123); + }); + + it('returns all icons in getIcons', () => { + expect(result.current.getIcons()).toEqual(mockedStateIcons); + expect(Object.keys(result.current.getIcons())).toHaveLength(3); + }); +}); diff --git a/packages/twenty-ui/src/display/icon/hooks/useIcons.ts b/packages/twenty-ui/src/display/icon/hooks/useIcons.ts new file mode 100644 index 0000000000..ff89b9717d --- /dev/null +++ b/packages/twenty-ui/src/display/icon/hooks/useIcons.ts @@ -0,0 +1,21 @@ +import { useAtomValue } from 'jotai'; + +import { Icon123 } from '@ui/display/icon/components/TablerIcons'; +import { iconsState } from '@ui/display/icon/states/iconsState'; + +export const useIcons = () => { + const icons = useAtomValue(iconsState); + const defaultIcon = Icon123; + + const getIcons = () => { + return icons; + }; + + const getIcon = (iconKey?: string | null, customDefaultIcon?: string) => { + return ( + icons[iconKey ?? ''] || icons[customDefaultIcon ?? ''] || defaultIcon + ); + }; + + return { getIcons, getIcon }; +}; diff --git a/packages/twenty-ui/src/display/icon/providers/IconsProvider.tsx b/packages/twenty-ui/src/display/icon/providers/IconsProvider.tsx new file mode 100644 index 0000000000..e2e4a318a7 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/providers/IconsProvider.tsx @@ -0,0 +1,20 @@ +import { useSetAtom } from 'jotai'; +import { useEffect } from 'react'; + +import { iconsState } from '@ui/display/icon/states/iconsState'; + +type IconsProviderProps = { + children: JSX.Element; +}; + +export const IconsProvider = ({ children }: IconsProviderProps) => { + const setIcons = useSetAtom(iconsState); + + useEffect(() => { + import('./internal/AllIcons').then(({ ALL_ICONS }) => { + setIcons(ALL_ICONS); + }); + }, [setIcons]); + + return children; +}; diff --git a/packages/twenty-ui/src/display/icon/providers/internal/AllIcons.ts b/packages/twenty-ui/src/display/icon/providers/internal/AllIcons.ts new file mode 100644 index 0000000000..ae7791c37a --- /dev/null +++ b/packages/twenty-ui/src/display/icon/providers/internal/AllIcons.ts @@ -0,0 +1,8370 @@ +/* oxlint-disable no-restricted-imports */ +import { + IconNumber123 as Icon123, + IconAB, + IconAB2, + IconABOff, + IconAbacus, + IconAbacusOff, + IconAbc, + IconAccessPoint, + IconAccessPointOff, + IconAccessible, + IconAccessibleOff, + IconActivity, + IconActivityHeartbeat, + IconAd, + IconAd2, + IconAdCircle, + IconAdCircleOff, + IconAdOff, + IconAddressBookOff, + IconAdjustments, + IconAdjustmentsAlt, + IconAdjustmentsBolt, + IconAdjustmentsCancel, + IconAdjustmentsCheck, + IconAdjustmentsCode, + IconAdjustmentsCog, + IconAdjustmentsDollar, + IconAdjustmentsDown, + IconAdjustmentsExclamation, + IconAdjustmentsHeart, + IconAdjustmentsHorizontal, + IconAdjustmentsMinus, + IconAdjustmentsOff, + IconAdjustmentsPause, + IconAdjustmentsPin, + IconAdjustmentsPlus, + IconAdjustmentsQuestion, + IconAdjustmentsSearch, + IconAdjustmentsShare, + IconAdjustmentsStar, + IconAdjustmentsUp, + IconAdjustmentsX, + IconAerialLift, + IconAffiliate, + IconAirBalloon, + IconAirConditioning, + IconAirConditioningDisabled, + IconAirTrafficControl, + IconAlarm, + IconAlarmMinus, + IconAlarmOff, + IconAlarmPlus, + IconAlarmSnooze, + IconAlbum, + IconAlbumOff, + IconAlertCircle, + IconAlertHexagon, + IconAlertOctagon, + IconAlertSmall, + IconAlertSquare, + IconAlertSquareRounded, + IconAlertTriangle, + IconAlien, + IconAlignBoxBottomCenter, + IconAlignBoxBottomLeft, + IconAlignBoxBottomRight, + IconAlignBoxCenterBottom, + IconAlignBoxCenterMiddle, + IconAlignBoxCenterStretch, + IconAlignBoxCenterTop, + IconAlignBoxLeftBottom, + IconAlignBoxLeftMiddle, + IconAlignBoxLeftStretch, + IconAlignBoxLeftTop, + IconAlignBoxRightBottom, + IconAlignBoxRightMiddle, + IconAlignBoxRightStretch, + IconAlignBoxRightTop, + IconAlignBoxTopCenter, + IconAlignBoxTopLeft, + IconAlignBoxTopRight, + IconAlignCenter, + IconAlignJustified, + IconAlignLeft, + IconAlignRight, + IconAlpha, + IconAlphabetCyrillic, + IconAlphabetGreek, + IconAlphabetLatin, + IconAmbulance, + IconAmpersand, + IconAnalyze, + IconAnalyzeOff, + IconAnchor, + IconAnchorOff, + IconAngle, + IconAnkh, + IconAntenna, + IconAntennaBars1, + IconAntennaBars2, + IconAntennaBars3, + IconAntennaBars4, + IconAntennaBars5, + IconAntennaBarsOff, + IconAntennaOff, + IconAperture, + IconApertureOff, + IconApi, + IconApiApp, + IconApiAppOff, + IconApiOff, + IconAppWindow, + IconApple, + IconApps, + IconAppsOff, + IconArchive, + IconArchiveOff, + IconArmchair, + IconArmchair2, + IconArmchair2Off, + IconArmchairOff, + IconArrowAutofitContent, + IconArrowAutofitDown, + IconArrowAutofitHeight, + IconArrowAutofitLeft, + IconArrowAutofitRight, + IconArrowAutofitUp, + IconArrowAutofitWidth, + IconArrowBack, + IconArrowBackUp, + IconArrowBackUpDouble, + IconArrowBadgeDown, + IconArrowBadgeLeft, + IconArrowBadgeRight, + IconArrowBadgeUp, + IconArrowBarBoth, + IconArrowBarDown, + IconArrowBarLeft, + IconArrowBarRight, + IconArrowBarToDown, + IconArrowBarToLeft, + IconArrowBarToRight, + IconArrowBarToUp, + IconArrowBarUp, + IconArrowBearLeft, + IconArrowBearLeft2, + IconArrowBearRight, + IconArrowBearRight2, + IconArrowBigDown, + IconArrowBigDownLine, + IconArrowBigDownLines, + IconArrowBigLeft, + IconArrowBigLeftLine, + IconArrowBigLeftLines, + IconArrowBigRight, + IconArrowBigRightLine, + IconArrowBigRightLines, + IconArrowBigUp, + IconArrowBigUpLine, + IconArrowBigUpLines, + IconArrowBounce, + IconArrowCapsule, + IconArrowCurveLeft, + IconArrowCurveRight, + IconArrowDown, + IconArrowDownBar, + IconArrowDownCircle, + IconArrowDownLeft, + IconArrowDownLeftCircle, + IconArrowDownRhombus, + IconArrowDownRight, + IconArrowDownRightCircle, + IconArrowDownSquare, + IconArrowDownTail, + IconArrowElbowLeft, + IconArrowElbowRight, + IconArrowFork, + IconArrowForward, + IconArrowForwardUp, + IconArrowForwardUpDouble, + IconArrowGuide, + IconArrowIteration, + IconArrowLeft, + IconArrowLeftBar, + IconArrowLeftCircle, + IconArrowLeftRhombus, + IconArrowLeftRight, + IconArrowLeftSquare, + IconArrowLeftTail, + IconArrowLoopLeft, + IconArrowLoopLeft2, + IconArrowLoopRight, + IconArrowLoopRight2, + IconArrowMerge, + IconArrowMergeBoth, + IconArrowMergeLeft, + IconArrowMergeRight, + IconArrowMoveDown, + IconArrowMoveLeft, + IconArrowMoveRight, + IconArrowMoveUp, + IconArrowNarrowDown, + IconArrowNarrowLeft, + IconArrowNarrowRight, + IconArrowNarrowUp, + IconArrowRampLeft, + IconArrowRampLeft2, + IconArrowRampLeft3, + IconArrowRampRight, + IconArrowRampRight2, + IconArrowRampRight3, + IconArrowRight, + IconArrowRightBar, + IconArrowRightCircle, + IconArrowRightRhombus, + IconArrowRightSquare, + IconArrowRightTail, + IconArrowRotaryFirstLeft, + IconArrowRotaryFirstRight, + IconArrowRotaryLastLeft, + IconArrowRotaryLastRight, + IconArrowRotaryLeft, + IconArrowRotaryRight, + IconArrowRotaryStraight, + IconArrowRoundaboutLeft, + IconArrowRoundaboutRight, + IconArrowSharpTurnLeft, + IconArrowSharpTurnRight, + IconArrowUp, + IconArrowUpBar, + IconArrowUpCircle, + IconArrowUpLeft, + IconArrowUpLeftCircle, + IconArrowUpRhombus, + IconArrowUpRight, + IconArrowUpRightCircle, + IconArrowUpSquare, + IconArrowUpTail, + IconArrowWaveLeftDown, + IconArrowWaveLeftUp, + IconArrowWaveRightDown, + IconArrowWaveRightUp, + IconArrowZigZag, + IconArrowsCross, + IconArrowsDiagonal, + IconArrowsDiagonal2, + IconArrowsDiagonalMinimize, + IconArrowsDiagonalMinimize2, + IconArrowsDiff, + IconArrowsDoubleNeSw, + IconArrowsDoubleNwSe, + IconArrowsDoubleSeNw, + IconArrowsDoubleSwNe, + IconArrowsDown, + IconArrowsDownUp, + IconArrowsExchange, + IconArrowsExchange2, + IconArrowsHorizontal, + IconArrowsJoin, + IconArrowsJoin2, + IconArrowsLeft, + IconArrowsLeftDown, + IconArrowsLeftRight, + IconArrowsMaximize, + IconArrowsMinimize, + IconArrowsMove, + IconArrowsMoveHorizontal, + IconArrowsMoveVertical, + IconArrowsRandom, + IconArrowsRight, + IconArrowsRightDown, + IconArrowsRightLeft, + IconArrowsShuffle, + IconArrowsShuffle2, + IconArrowsSort, + IconArrowsSplit, + IconArrowsSplit2, + IconArrowsTransferDown, + IconArrowsTransferUp, + IconArrowsUp, + IconArrowsUpDown, + IconArrowsUpLeft, + IconArrowsUpRight, + IconArrowsVertical, + IconArtboard, + IconArtboardOff, + IconArticle, + IconArticleOff, + IconAspectRatio, + IconAspectRatioOff, + IconAssembly, + IconAssemblyOff, + IconAsset, + IconAsterisk, + IconAsteriskSimple, + IconAt, + IconAtOff, + IconAtom, + IconAtom2, + IconAtomOff, + IconAugmentedReality, + IconAugmentedReality2, + IconAugmentedRealityOff, + IconAuth2fa, + IconAward, + IconAwardOff, + IconAxe, + IconAxisX, + IconAxisY, + IconBabyBottle, + IconBabyCarriage, + IconBackhoe, + IconBackpack, + IconBackpackOff, + IconBackslash, + IconBackspace, + IconBadge, + IconBadge2k, + IconBadge3d, + IconBadge4k, + IconBadge8k, + IconBadgeAd, + IconBadgeAr, + IconBadgeCc, + IconBadgeHd, + IconBadgeOff, + IconBadgeSd, + IconBadgeTm, + IconBadgeVo, + IconBadgeVr, + IconBadgeWc, + IconBadges, + IconBadgesOff, + IconBaguette, + IconBallAmericanFootball, + IconBallAmericanFootballOff, + IconBallBaseball, + IconBallBasketball, + IconBallBowling, + IconBallFootball, + IconBallFootballOff, + IconBallTennis, + IconBallVolleyball, + IconBalloon, + IconBalloonOff, + IconBallpen, + IconBallpenOff, + IconBan, + IconBandage, + IconBandageOff, + IconBarbell, + IconBarbellOff, + IconBarcode, + IconBarcodeOff, + IconBarrel, + IconBarrelOff, + IconBarrierBlock, + IconBarrierBlockOff, + IconBaseline, + IconBaselineDensityLarge, + IconBaselineDensityMedium, + IconBaselineDensitySmall, + IconBasket, + IconBasketBolt, + IconBasketCancel, + IconBasketCheck, + IconBasketCode, + IconBasketCog, + IconBasketDiscount, + IconBasketDollar, + IconBasketDown, + IconBasketExclamation, + IconBasketHeart, + IconBasketMinus, + IconBasketOff, + IconBasketPause, + IconBasketPin, + IconBasketPlus, + IconBasketQuestion, + IconBasketSearch, + IconBasketShare, + IconBasketStar, + IconBasketUp, + IconBasketX, + IconBat, + IconBath, + IconBathOff, + IconBattery, + IconBattery1, + IconBattery2, + IconBattery3, + IconBattery4, + IconBatteryAutomotive, + IconBatteryCharging, + IconBatteryCharging2, + IconBatteryEco, + IconBatteryOff, + IconBeach, + IconBeachOff, + IconBed, + IconBedOff, + IconBeer, + IconBeerOff, + IconBell, + IconBellBolt, + IconBellCancel, + IconBellCheck, + IconBellCode, + IconBellCog, + IconBellDollar, + IconBellDown, + IconBellExclamation, + IconBellHeart, + IconBellMinus, + IconBellOff, + IconBellPause, + IconBellPin, + IconBellPlus, + IconBellQuestion, + IconBellRinging, + IconBellRinging2, + IconBellSchool, + IconBellSearch, + IconBellShare, + IconBellStar, + IconBellUp, + IconBellX, + IconBellZ, + IconBeta, + IconBible, + IconBike, + IconBikeOff, + IconBinary, + IconBinaryOff, + IconBinaryTree, + IconBinaryTree2, + IconBiohazard, + IconBiohazardOff, + IconBlade, + IconBleach, + IconBleachChlorine, + IconBleachNoChlorine, + IconBleachOff, + IconBlockquote, + IconBluetooth, + IconBluetoothConnected, + IconBluetoothOff, + IconBluetoothX, + IconBlur, + IconBlurOff, + IconBmp, + IconBold, + IconBoldOff, + IconBolt, + IconBoltOff, + IconBomb, + IconBone, + IconBoneOff, + IconBong, + IconBongOff, + IconBook, + IconBook2, + IconBookDownload, + IconBookOff, + IconBookUpload, + IconBookmark, + IconBookmarkEdit, + IconBookmarkMinus, + IconBookmarkOff, + IconBookmarkPlus, + IconBookmarkQuestion, + IconBookmarks, + IconBookmarksOff, + IconBooks, + IconBooksOff, + IconBorderAll, + IconBorderBottom, + IconBorderCorners, + IconBorderHorizontal, + IconBorderInner, + IconBorderLeft, + IconBorderNone, + IconBorderOuter, + IconBorderRadius, + IconBorderRight, + IconBorderSides, + IconBorderStyle, + IconBorderStyle2, + IconBorderTop, + IconBorderVertical, + IconBottle, + IconBottleOff, + IconBounceLeft, + IconBounceRight, + IconBow, + IconBowl, + IconBox, + IconBoxAlignBottom, + IconBoxAlignBottomLeft, + IconBoxAlignBottomRight, + IconBoxAlignLeft, + IconBoxAlignRight, + IconBoxAlignTop, + IconBoxAlignTopLeft, + IconBoxAlignTopRight, + IconBoxMargin, + IconBoxModel, + IconBoxModel2, + IconBoxModel2Off, + IconBoxModelOff, + IconBoxMultiple, + IconBoxMultiple0, + IconBoxMultiple1, + IconBoxMultiple2, + IconBoxMultiple3, + IconBoxMultiple4, + IconBoxMultiple5, + IconBoxMultiple6, + IconBoxMultiple7, + IconBoxMultiple8, + IconBoxMultiple9, + IconBoxOff, + IconBoxPadding, + IconBraces, + IconBracesOff, + IconBrackets, + IconBracketsContain, + IconBracketsContainEnd, + IconBracketsContainStart, + IconBracketsOff, + IconBraille, + IconBrain, + IconBrand4chan, + IconBrandAbstract, + IconBrandAdobe, + IconBrandAdonisJs, + IconBrandAirbnb, + IconBrandAirtable, + IconBrandAlgolia, + IconBrandAlipay, + IconBrandAlpineJs, + IconBrandAmazon, + IconBrandAmd, + IconBrandAmigo, + IconBrandAmongUs, + IconBrandAndroid, + IconBrandAngular, + IconBrandAnsible, + IconBrandAo3, + IconBrandAppgallery, + IconBrandApple, + IconBrandAppleArcade, + IconBrandApplePodcast, + IconBrandAppstore, + IconBrandAsana, + IconBrandAws, + IconBrandAzure, + IconBrandBackbone, + IconBrandBadoo, + IconBrandBaidu, + IconBrandBandcamp, + IconBrandBandlab, + IconBrandBeats, + IconBrandBehance, + IconBrandBilibili, + IconBrandBinance, + IconBrandBing, + IconBrandBitbucket, + IconBrandBlackberry, + IconBrandBlender, + IconBrandBlogger, + IconBrandBooking, + IconBrandBootstrap, + IconBrandBulma, + IconBrandBumble, + IconBrandBunpo, + IconBrandCSharp, + IconBrandCake, + IconBrandCakephp, + IconBrandCampaignmonitor, + IconBrandCarbon, + IconBrandCashapp, + IconBrandChrome, + IconBrandCinema4d, + IconBrandCitymapper, + IconBrandCloudflare, + IconBrandCodecov, + IconBrandCodepen, + IconBrandCodesandbox, + IconBrandCohost, + IconBrandCoinbase, + IconBrandComedyCentral, + IconBrandCoreos, + IconBrandCouchdb, + IconBrandCouchsurfing, + IconBrandCpp, + IconBrandCraft, + IconBrandCrunchbase, + IconBrandCss3, + IconBrandCtemplar, + IconBrandCucumber, + IconBrandCupra, + IconBrandCypress, + IconBrandD3, + IconBrandDaysCounter, + IconBrandDcos, + IconBrandDebian, + IconBrandDeezer, + IconBrandDeliveroo, + IconBrandDeno, + IconBrandDenodo, + IconBrandDeviantart, + IconBrandDigg, + IconBrandDingtalk, + IconBrandDiscord, + IconBrandDisney, + IconBrandDisqus, + IconBrandDjango, + IconBrandDocker, + IconBrandDoctrine, + IconBrandDolbyDigital, + IconBrandDouban, + IconBrandDribbble, + IconBrandDrops, + IconBrandDrupal, + IconBrandEdge, + IconBrandElastic, + IconBrandElectronicArts, + IconBrandEmber, + IconBrandEnvato, + IconBrandEtsy, + IconBrandEvernote, + IconBrandFacebook, + IconBrandFeedly, + IconBrandFigma, + IconBrandFilezilla, + IconBrandFinder, + IconBrandFirebase, + IconBrandFirefox, + IconBrandFiverr, + IconBrandFlickr, + IconBrandFlightradar24, + IconBrandFlipboard, + IconBrandFlutter, + IconBrandFortnite, + IconBrandFoursquare, + IconBrandFramer, + IconBrandFramerMotion, + IconBrandFunimation, + IconBrandGatsby, + IconBrandGit, + IconBrandGithub, + IconBrandGithubCopilot, + IconBrandGitlab, + IconBrandGmail, + IconBrandGolang, + IconBrandGoogle, + IconBrandGoogleAnalytics, + IconBrandGoogleBigQuery, + IconBrandGoogleDrive, + IconBrandGoogleFit, + IconBrandGoogleHome, + IconBrandGoogleMaps, + IconBrandGoogleOne, + IconBrandGooglePhotos, + IconBrandGooglePlay, + IconBrandGooglePodcasts, + IconBrandGrammarly, + IconBrandGraphql, + IconBrandGravatar, + IconBrandGrindr, + IconBrandGuardian, + IconBrandGumroad, + IconBrandHbo, + IconBrandHeadlessui, + IconBrandHexo, + IconBrandHipchat, + IconBrandHtml5, + IconBrandInertia, + IconBrandInstagram, + IconBrandIntercom, + IconBrandItch, + IconBrandJavascript, + IconBrandJuejin, + IconBrandKbin, + IconBrandKick, + IconBrandKickstarter, + IconBrandKotlin, + IconBrandLaravel, + IconBrandLastfm, + IconBrandLeetcode, + IconBrandLetterboxd, + IconBrandLine, + IconBrandLinkedin, + IconBrandLinktree, + IconBrandLinqpad, + IconBrandLoom, + IconBrandMailgun, + IconBrandMantine, + IconBrandMastercard, + IconBrandMastodon, + IconBrandMatrix, + IconBrandMcdonalds, + IconBrandMedium, + IconBrandMercedes, + IconBrandMessenger, + IconBrandMeta, + IconBrandMinecraft, + IconBrandMiniprogram, + IconBrandMixpanel, + IconBrandMonday, + IconBrandMongodb, + IconBrandMyOppo, + IconBrandMysql, + IconBrandNationalGeographic, + IconBrandNem, + IconBrandNetbeans, + IconBrandNeteaseMusic, + IconBrandNetflix, + IconBrandNexo, + IconBrandNextcloud, + IconBrandNextjs, + IconBrandNodejs, + IconBrandNordVpn, + IconBrandNotion, + IconBrandNpm, + IconBrandNuxt, + IconBrandNytimes, + IconBrandOauth, + IconBrandOffice, + IconBrandOkRu, + IconBrandOnedrive, + IconBrandOnlyfans, + IconBrandOpenSource, + IconBrandOpenai, + IconBrandOpenvpn, + IconBrandOpera, + IconBrandPagekit, + IconBrandPatreon, + IconBrandPaypal, + IconBrandPaypay, + IconBrandPeanut, + IconBrandPepsi, + IconBrandPhp, + IconBrandPicsart, + IconBrandPinterest, + IconBrandPlanetscale, + IconBrandPocket, + IconBrandPolymer, + IconBrandPowershell, + IconBrandProducthunt, + IconBrandPushbullet, + IconBrandPushover, + IconBrandPython, + IconBrandQq, + IconBrandRadixUi, + IconBrandReact, + IconBrandReactNative, + IconBrandReason, + IconBrandReddit, + IconBrandRedhat, + IconBrandRedux, + IconBrandRevolut, + IconBrandRumble, + IconBrandRust, + IconBrandSafari, + IconBrandSamsungpass, + IconBrandSass, + IconBrandSentry, + IconBrandSharik, + IconBrandShazam, + IconBrandShopee, + IconBrandSketch, + IconBrandSkype, + IconBrandSlack, + IconBrandSnapchat, + IconBrandSnapseed, + IconBrandSnowflake, + IconBrandSocketIo, + IconBrandSolidjs, + IconBrandSoundcloud, + IconBrandSpacehey, + IconBrandSpeedtest, + IconBrandSpotify, + IconBrandStackoverflow, + IconBrandStackshare, + IconBrandSteam, + IconBrandStorj, + IconBrandStorybook, + IconBrandStorytel, + IconBrandStrava, + IconBrandStripe, + IconBrandSublimeText, + IconBrandSugarizer, + IconBrandSupabase, + IconBrandSuperhuman, + IconBrandSupernova, + IconBrandSurfshark, + IconBrandSvelte, + IconBrandSwift, + IconBrandSymfony, + IconBrandTabler, + IconBrandTailwind, + IconBrandTaobao, + IconBrandTed, + IconBrandTelegram, + IconBrandTerraform, + IconBrandTether, + IconBrandThreads, + IconBrandThreejs, + IconBrandTidal, + IconBrandTiktok, + IconBrandTinder, + IconBrandTopbuzz, + IconBrandTorchain, + IconBrandToyota, + IconBrandTrello, + IconBrandTripadvisor, + IconBrandTumblr, + IconBrandTwilio, + IconBrandTwitch, + IconBrandTwitter, + IconBrandTypescript, + IconBrandUber, + IconBrandUbuntu, + IconBrandUnity, + IconBrandUnsplash, + IconBrandUpwork, + IconBrandValorant, + IconBrandVercel, + IconBrandVimeo, + IconBrandVinted, + IconBrandVisa, + IconBrandVisualStudio, + IconBrandVite, + IconBrandVivaldi, + IconBrandVk, + IconBrandVlc, + IconBrandVolkswagen, + IconBrandVsco, + IconBrandVscode, + IconBrandVue, + IconBrandWalmart, + IconBrandWaze, + IconBrandWebflow, + IconBrandWechat, + IconBrandWeibo, + IconBrandWhatsapp, + IconBrandWikipedia, + IconBrandWindows, + IconBrandWindy, + IconBrandWish, + IconBrandWix, + IconBrandWordpress, + IconBrandX, + IconBrandXamarin, + IconBrandXbox, + IconBrandXdeep, + IconBrandXing, + IconBrandYahoo, + IconBrandYandex, + IconBrandYatse, + IconBrandYcombinator, + IconBrandYoutube, + IconBrandYoutubeKids, + IconBrandZalando, + IconBrandZapier, + IconBrandZeit, + IconBrandZhihu, + IconBrandZoom, + IconBrandZulip, + IconBrandZwift, + IconBread, + IconBreadOff, + IconBriefcase, + IconBriefcase2, + IconBriefcaseOff, + IconBrightness, + IconBrightness2, + IconBrightnessDown, + IconBrightnessHalf, + IconBrightnessOff, + IconBrightnessUp, + IconBroadcast, + IconBroadcastOff, + IconBrowser, + IconBrowserCheck, + IconBrowserMaximize, + IconBrowserOff, + IconBrowserPlus, + IconBrowserX, + IconBrush, + IconBrushOff, + IconBucket, + IconBucketDroplet, + IconBucketOff, + IconBug, + IconBugOff, + IconBuilding, + IconBuildingArch, + IconBuildingBank, + IconBuildingBridge, + IconBuildingBridge2, + IconBuildingBroadcastTower, + IconBuildingCarousel, + IconBuildingCastle, + IconBuildingChurch, + IconBuildingCircus, + IconBuildingCommunity, + IconBuildingCottage, + IconBuildingEstate, + IconBuildingFactory, + IconBuildingFactory2, + IconBuildingFortress, + IconBuildingHospital, + IconBuildingLighthouse, + IconBuildingMonument, + IconBuildingMosque, + IconBuildingPavilion, + IconBuildingSkyscraper, + IconBuildingStadium, + IconBuildingStore, + IconBuildingTunnel, + IconBuildingWarehouse, + IconBuildingWindTurbine, + IconBulb, + IconBulbOff, + IconBulldozer, + IconBus, + IconBusOff, + IconBusStop, + IconBusinessplan, + IconButterfly, + IconCactus, + IconCactusOff, + IconCake, + IconCakeOff, + IconCalculator, + IconCalculatorOff, + IconCalendar, + IconCalendarBolt, + IconCalendarCancel, + IconCalendarCheck, + IconCalendarClock, + IconCalendarCode, + IconCalendarCog, + IconCalendarDollar, + IconCalendarDown, + IconCalendarDue, + IconCalendarEvent, + IconCalendarExclamation, + IconCalendarHeart, + IconCalendarMinus, + IconCalendarOff, + IconCalendarPause, + IconCalendarPin, + IconCalendarPlus, + IconCalendarQuestion, + IconCalendarRepeat, + IconCalendarSearch, + IconCalendarShare, + IconCalendarStar, + IconCalendarStats, + IconCalendarTime, + IconCalendarUp, + IconCalendarX, + IconCamera, + IconCameraBolt, + IconCameraCancel, + IconCameraCheck, + IconCameraCode, + IconCameraCog, + IconCameraDollar, + IconCameraDown, + IconCameraExclamation, + IconCameraHeart, + IconCameraMinus, + IconCameraOff, + IconCameraPause, + IconCameraPin, + IconCameraPlus, + IconCameraQuestion, + IconCameraRotate, + IconCameraSearch, + IconCameraSelfie, + IconCameraShare, + IconCameraStar, + IconCameraUp, + IconCameraX, + IconCamper, + IconCampfire, + IconCancel, + IconCandle, + IconCandy, + IconCandyOff, + IconCane, + IconCannabis, + IconCapsule, + IconCapsuleHorizontal, + IconCapture, + IconCaptureOff, + IconCar, + IconCarCrane, + IconCarCrash, + IconCarOff, + IconCarTurbine, + IconCaravan, + IconCardboards, + IconCardboardsOff, + IconCards, + IconCaretDown, + IconCaretLeft, + IconCaretRight, + IconCaretUp, + IconCarouselHorizontal, + IconCarouselVertical, + IconCarrot, + IconCarrotOff, + IconCash, + IconCashBanknote, + IconCashBanknoteOff, + IconCashOff, + IconCast, + IconCastOff, + IconCat, + IconCategory, + IconCategory2, + IconCe, + IconCeOff, + IconCell, + IconCellSignal1, + IconCellSignal2, + IconCellSignal3, + IconCellSignal4, + IconCellSignal5, + IconCellSignalOff, + IconCertificate, + IconCertificate2, + IconCertificate2Off, + IconCertificateOff, + IconChairDirector, + IconChalkboard, + IconChalkboardOff, + IconChargingPile, + IconChartArcs, + IconChartArcs3, + IconChartArea, + IconChartAreaLine, + IconChartArrows, + IconChartArrowsVertical, + IconChartBar, + IconChartBarOff, + IconChartBubble, + IconChartCandle, + IconChartCircles, + IconChartDonut, + IconChartDonut2, + IconChartDonut3, + IconChartDonut4, + IconChartDots, + IconChartDots2, + IconChartDots3, + IconChartGridDots, + IconChartHistogram, + IconChartInfographic, + IconChartLine, + IconChartPie, + IconChartPie2, + IconChartPie3, + IconChartPie4, + IconChartPieOff, + IconChartPpf, + IconChartRadar, + IconChartSankey, + IconChartTreemap, + IconCheck, + IconCheckbox, + IconChecklist, + IconChecks, + IconCheckupList, + IconCheese, + IconChefHat, + IconChefHatOff, + IconCherry, + IconChess, + IconChessBishop, + IconChessKing, + IconChessKnight, + IconChessQueen, + IconChessRook, + IconChevronCompactDown, + IconChevronCompactLeft, + IconChevronCompactRight, + IconChevronCompactUp, + IconChevronDown, + IconChevronDownLeft, + IconChevronDownRight, + IconChevronLeft, + IconChevronLeftPipe, + IconChevronRight, + IconChevronRightPipe, + IconChevronUp, + IconChevronUpLeft, + IconChevronUpRight, + IconChevronsDown, + IconChevronsDownLeft, + IconChevronsDownRight, + IconChevronsLeft, + IconChevronsRight, + IconChevronsUp, + IconChevronsUpLeft, + IconChevronsUpRight, + IconChisel, + IconChristmasTree, + IconChristmasTreeOff, + IconCircle, + IconCircleArrowDown, + IconCircleArrowDownLeft, + IconCircleArrowDownRight, + IconCircleArrowLeft, + IconCircleArrowRight, + IconCircleArrowUp, + IconCircleArrowUpLeft, + IconCircleArrowUpRight, + IconCircleCaretDown, + IconCircleCaretLeft, + IconCircleCaretRight, + IconCircleCaretUp, + IconCircleCheck, + IconCircleChevronDown, + IconCircleChevronLeft, + IconCircleChevronRight, + IconCircleChevronUp, + IconCircleChevronsDown, + IconCircleChevronsLeft, + IconCircleChevronsRight, + IconCircleChevronsUp, + IconCircleDashed, + IconCircleDot, + IconCircleDotted, + IconCircleHalf, + IconCircleHalf2, + IconCircleHalfVertical, + IconCircleKey, + IconCircleLetterA, + IconCircleLetterB, + IconCircleLetterC, + IconCircleLetterD, + IconCircleLetterE, + IconCircleLetterF, + IconCircleLetterG, + IconCircleLetterH, + IconCircleLetterI, + IconCircleLetterJ, + IconCircleLetterK, + IconCircleLetterL, + IconCircleLetterM, + IconCircleLetterN, + IconCircleLetterO, + IconCircleLetterP, + IconCircleLetterQ, + IconCircleLetterR, + IconCircleLetterS, + IconCircleLetterT, + IconCircleLetterU, + IconCircleLetterV, + IconCircleLetterW, + IconCircleLetterX, + IconCircleLetterY, + IconCircleLetterZ, + IconCircleMinus, + IconCircleNumber0, + IconCircleNumber1, + IconCircleNumber2, + IconCircleNumber3, + IconCircleNumber4, + IconCircleNumber5, + IconCircleNumber6, + IconCircleNumber7, + IconCircleNumber8, + IconCircleNumber9, + IconCircleOff, + IconCirclePlus, + IconCircleRectangle, + IconCircleRectangleOff, + IconCircleSquare, + IconCircleTriangle, + IconCircleX, + IconCircles, + IconCirclesRelation, + IconCircuitAmmeter, + IconCircuitBattery, + IconCircuitBulb, + IconCircuitCapacitor, + IconCircuitCapacitorPolarized, + IconCircuitCell, + IconCircuitCellPlus, + IconCircuitChangeover, + IconCircuitDiode, + IconCircuitDiodeZener, + IconCircuitGround, + IconCircuitGroundDigital, + IconCircuitInductor, + IconCircuitMotor, + IconCircuitPushbutton, + IconCircuitResistor, + IconCircuitSwitchClosed, + IconCircuitSwitchOpen, + IconCircuitVoltmeter, + IconClearAll, + IconClearFormatting, + IconClick, + IconClipboard, + IconClipboardCheck, + IconClipboardCopy, + IconClipboardData, + IconClipboardHeart, + IconClipboardList, + IconClipboardOff, + IconClipboardPlus, + IconClipboardText, + IconClipboardTypography, + IconClipboardX, + IconClock, + IconClock2, + IconClockBolt, + IconClockCancel, + IconClockCheck, + IconClockCode, + IconClockCog, + IconClockDollar, + IconClockDown, + IconClockEdit, + IconClockExclamation, + IconClockHeart, + IconClockHour1, + IconClockHour10, + IconClockHour11, + IconClockHour12, + IconClockHour2, + IconClockHour3, + IconClockHour4, + IconClockHour5, + IconClockHour6, + IconClockHour7, + IconClockHour8, + IconClockHour9, + IconClockMinus, + IconClockOff, + IconClockPause, + IconClockPin, + IconClockPlay, + IconClockPlus, + IconClockQuestion, + IconClockRecord, + IconClockSearch, + IconClockShare, + IconClockShield, + IconClockStar, + IconClockStop, + IconClockUp, + IconClockX, + IconClothesRack, + IconClothesRackOff, + IconCloud, + IconCloudBolt, + IconCloudCancel, + IconCloudCheck, + IconCloudCode, + IconCloudCog, + IconCloudComputing, + IconCloudDataConnection, + IconCloudDollar, + IconCloudDown, + IconCloudDownload, + IconCloudExclamation, + IconCloudFog, + IconCloudHeart, + IconCloudLock, + IconCloudLockOpen, + IconCloudMinus, + IconCloudOff, + IconCloudPause, + IconCloudPin, + IconCloudPlus, + IconCloudQuestion, + IconCloudRain, + IconCloudSearch, + IconCloudShare, + IconCloudSnow, + IconCloudStar, + IconCloudStorm, + IconCloudUp, + IconCloudUpload, + IconCloudX, + IconClover, + IconClover2, + IconClubs, + IconCode, + IconCodeCircle, + IconCodeCircle2, + IconCodeDots, + IconCodeMinus, + IconCodeOff, + IconCodePlus, + IconCoffee, + IconCoffeeOff, + IconCoffin, + IconCoin, + IconCoinBitcoin, + IconCoinEuro, + IconCoinMonero, + IconCoinOff, + IconCoinPound, + IconCoinRupee, + IconCoinYen, + IconCoinYuan, + IconCoins, + IconColorFilter, + IconColorPicker, + IconColorPickerOff, + IconColorSwatch, + IconColorSwatchOff, + IconColumnInsertLeft, + IconColumnInsertRight, + IconColumnRemove, + IconColumns, + IconColumns1, + IconColumns2, + IconColumns3, + IconColumnsOff, + IconComet, + IconCommand, + IconCommandOff, + IconCompass, + IconCompassOff, + IconComponents, + IconComponentsOff, + IconCone, + IconCone2, + IconConeOff, + IconConePlus, + IconConfetti, + IconConfettiOff, + IconConfucius, + IconContainer, + IconContainerOff, + IconContrast, + IconContrast2, + IconContrast2Off, + IconContrastOff, + IconCooker, + IconCookie, + IconCookieMan, + IconCookieOff, + IconCopy, + IconCopyOff, + IconCopyleft, + IconCopyleftOff, + IconCopyright, + IconCopyrightOff, + IconCornerDownLeft, + IconCornerDownLeftDouble, + IconCornerDownRight, + IconCornerDownRightDouble, + IconCornerLeftDown, + IconCornerLeftDownDouble, + IconCornerLeftUp, + IconCornerLeftUpDouble, + IconCornerRightDown, + IconCornerRightDownDouble, + IconCornerRightUp, + IconCornerRightUpDouble, + IconCornerUpLeft, + IconCornerUpLeftDouble, + IconCornerUpRight, + IconCornerUpRightDouble, + IconCpu, + IconCpu2, + IconCpuOff, + IconCrane, + IconCraneOff, + IconCreativeCommons, + IconCreativeCommonsBy, + IconCreativeCommonsNc, + IconCreativeCommonsNd, + IconCreativeCommonsOff, + IconCreativeCommonsSa, + IconCreativeCommonsZero, + IconCreditCard, + IconCreditCardOff, + IconCricket, + IconCrop, + IconCross, + IconCrossOff, + IconCrosshair, + IconCrown, + IconCrownOff, + IconCrutches, + IconCrutchesOff, + IconCrystalBall, + IconCsv, + IconCube, + IconCube3dSphere, + IconCube3dSphereOff, + IconCubeOff, + IconCubePlus, + IconCubeSend, + IconCubeUnfolded, + IconCup, + IconCupOff, + IconCurling, + IconCurlyLoop, + IconCurrency, + IconCurrencyAfghani, + IconCurrencyBahraini, + IconCurrencyBaht, + IconCurrencyBitcoin, + IconCurrencyCent, + IconCurrencyDinar, + IconCurrencyDirham, + IconCurrencyDogecoin, + IconCurrencyDollar, + IconCurrencyDollarAustralian, + IconCurrencyDollarBrunei, + IconCurrencyDollarCanadian, + IconCurrencyDollarGuyanese, + IconCurrencyDollarOff, + IconCurrencyDollarSingapore, + IconCurrencyDollarZimbabwean, + IconCurrencyDong, + IconCurrencyDram, + IconCurrencyEthereum, + IconCurrencyEuro, + IconCurrencyEuroOff, + IconCurrencyFlorin, + IconCurrencyForint, + IconCurrencyFrank, + IconCurrencyGuarani, + IconCurrencyHryvnia, + IconCurrencyIranianRial, + IconCurrencyKip, + IconCurrencyKroneCzech, + IconCurrencyKroneDanish, + IconCurrencyKroneSwedish, + IconCurrencyLari, + IconCurrencyLeu, + IconCurrencyLira, + IconCurrencyLitecoin, + IconCurrencyLyd, + IconCurrencyManat, + IconCurrencyMonero, + IconCurrencyNaira, + IconCurrencyNano, + IconCurrencyOff, + IconCurrencyPaanga, + IconCurrencyPeso, + IconCurrencyPound, + IconCurrencyPoundOff, + IconCurrencyQuetzal, + IconCurrencyReal, + IconCurrencyRenminbi, + IconCurrencyRipple, + IconCurrencyRiyal, + IconCurrencyRubel, + IconCurrencyRufiyaa, + IconCurrencyRupee, + IconCurrencyRupeeNepalese, + IconCurrencyShekel, + IconCurrencySolana, + IconCurrencySom, + IconCurrencyTaka, + IconCurrencyTenge, + IconCurrencyTugrik, + IconCurrencyWon, + IconCurrencyYen, + IconCurrencyYenOff, + IconCurrencyYuan, + IconCurrencyZloty, + IconCurrentLocation, + IconCurrentLocationOff, + IconCursorOff, + IconCursorText, + IconCut, + IconCylinder, + IconCylinderOff, + IconCylinderPlus, + IconDashboard, + IconDashboardOff, + IconDatabase, + IconDatabaseCog, + IconDatabaseDollar, + IconDatabaseEdit, + IconDatabaseExclamation, + IconDatabaseExport, + IconDatabaseHeart, + IconDatabaseImport, + IconDatabaseLeak, + IconDatabaseMinus, + IconDatabaseOff, + IconDatabasePlus, + IconDatabaseSearch, + IconDatabaseShare, + IconDatabaseStar, + IconDatabaseX, + IconDecimal, + IconDeer, + IconDelta, + IconDental, + IconDentalBroken, + IconDentalOff, + IconDeselect, + IconDetails, + IconDetailsOff, + IconDeviceAirpods, + IconDeviceAirpodsCase, + IconDeviceAirtag, + IconDeviceAnalytics, + IconDeviceAudioTape, + IconDeviceCameraPhone, + IconDeviceCctv, + IconDeviceCctvOff, + IconDeviceComputerCamera, + IconDeviceComputerCameraOff, + IconDeviceDesktop, + IconDeviceDesktopAnalytics, + IconDeviceDesktopBolt, + IconDeviceDesktopCancel, + IconDeviceDesktopCheck, + IconDeviceDesktopCode, + IconDeviceDesktopCog, + IconDeviceDesktopDollar, + IconDeviceDesktopDown, + IconDeviceDesktopExclamation, + IconDeviceDesktopHeart, + IconDeviceDesktopMinus, + IconDeviceDesktopOff, + IconDeviceDesktopPause, + IconDeviceDesktopPin, + IconDeviceDesktopPlus, + IconDeviceDesktopQuestion, + IconDeviceDesktopSearch, + IconDeviceDesktopShare, + IconDeviceDesktopStar, + IconDeviceDesktopUp, + IconDeviceDesktopX, + IconDeviceFloppy, + IconDeviceGamepad, + IconDeviceGamepad2, + IconDeviceHeartMonitor, + IconDeviceImac, + IconDeviceImacBolt, + IconDeviceImacCancel, + IconDeviceImacCheck, + IconDeviceImacCode, + IconDeviceImacCog, + IconDeviceImacDollar, + IconDeviceImacDown, + IconDeviceImacExclamation, + IconDeviceImacHeart, + IconDeviceImacMinus, + IconDeviceImacOff, + IconDeviceImacPause, + IconDeviceImacPin, + IconDeviceImacPlus, + IconDeviceImacQuestion, + IconDeviceImacSearch, + IconDeviceImacShare, + IconDeviceImacStar, + IconDeviceImacUp, + IconDeviceImacX, + IconDeviceIpad, + IconDeviceIpadBolt, + IconDeviceIpadCancel, + IconDeviceIpadCheck, + IconDeviceIpadCode, + IconDeviceIpadCog, + IconDeviceIpadDollar, + IconDeviceIpadDown, + IconDeviceIpadExclamation, + IconDeviceIpadHeart, + IconDeviceIpadHorizontal, + IconDeviceIpadHorizontalBolt, + IconDeviceIpadHorizontalCancel, + IconDeviceIpadHorizontalCheck, + IconDeviceIpadHorizontalCode, + IconDeviceIpadHorizontalCog, + IconDeviceIpadHorizontalDollar, + IconDeviceIpadHorizontalDown, + IconDeviceIpadHorizontalExclamation, + IconDeviceIpadHorizontalHeart, + IconDeviceIpadHorizontalMinus, + IconDeviceIpadHorizontalOff, + IconDeviceIpadHorizontalPause, + IconDeviceIpadHorizontalPin, + IconDeviceIpadHorizontalPlus, + IconDeviceIpadHorizontalQuestion, + IconDeviceIpadHorizontalSearch, + IconDeviceIpadHorizontalShare, + IconDeviceIpadHorizontalStar, + IconDeviceIpadHorizontalUp, + IconDeviceIpadHorizontalX, + IconDeviceIpadMinus, + IconDeviceIpadOff, + IconDeviceIpadPause, + IconDeviceIpadPin, + IconDeviceIpadPlus, + IconDeviceIpadQuestion, + IconDeviceIpadSearch, + IconDeviceIpadShare, + IconDeviceIpadStar, + IconDeviceIpadUp, + IconDeviceIpadX, + IconDeviceLandlinePhone, + IconDeviceLaptop, + IconDeviceLaptopOff, + IconDeviceMobile, + IconDeviceMobileBolt, + IconDeviceMobileCancel, + IconDeviceMobileCharging, + IconDeviceMobileCheck, + IconDeviceMobileCode, + IconDeviceMobileCog, + IconDeviceMobileDollar, + IconDeviceMobileDown, + IconDeviceMobileExclamation, + IconDeviceMobileHeart, + IconDeviceMobileMessage, + IconDeviceMobileMinus, + IconDeviceMobileOff, + IconDeviceMobilePause, + IconDeviceMobilePin, + IconDeviceMobilePlus, + IconDeviceMobileQuestion, + IconDeviceMobileRotated, + IconDeviceMobileSearch, + IconDeviceMobileShare, + IconDeviceMobileStar, + IconDeviceMobileUp, + IconDeviceMobileVibration, + IconDeviceMobileX, + IconDeviceNintendo, + IconDeviceNintendoOff, + IconDeviceProjector, + IconDeviceRemote, + IconDeviceSdCard, + IconDeviceSim, + IconDeviceSim1, + IconDeviceSim2, + IconDeviceSim3, + IconDeviceSpeaker, + IconDeviceSpeakerOff, + IconDeviceTablet, + IconDeviceTabletBolt, + IconDeviceTabletCancel, + IconDeviceTabletCheck, + IconDeviceTabletCode, + IconDeviceTabletCog, + IconDeviceTabletDollar, + IconDeviceTabletDown, + IconDeviceTabletExclamation, + IconDeviceTabletHeart, + IconDeviceTabletMinus, + IconDeviceTabletOff, + IconDeviceTabletPause, + IconDeviceTabletPin, + IconDeviceTabletPlus, + IconDeviceTabletQuestion, + IconDeviceTabletSearch, + IconDeviceTabletShare, + IconDeviceTabletStar, + IconDeviceTabletUp, + IconDeviceTabletX, + IconDeviceTv, + IconDeviceTvOff, + IconDeviceTvOld, + IconDeviceVisionPro, + IconDeviceWatch, + IconDeviceWatchBolt, + IconDeviceWatchCancel, + IconDeviceWatchCheck, + IconDeviceWatchCode, + IconDeviceWatchCog, + IconDeviceWatchDollar, + IconDeviceWatchDown, + IconDeviceWatchExclamation, + IconDeviceWatchHeart, + IconDeviceWatchMinus, + IconDeviceWatchOff, + IconDeviceWatchPause, + IconDeviceWatchPin, + IconDeviceWatchPlus, + IconDeviceWatchQuestion, + IconDeviceWatchSearch, + IconDeviceWatchShare, + IconDeviceWatchStar, + IconDeviceWatchStats, + IconDeviceWatchStats2, + IconDeviceWatchUp, + IconDeviceWatchX, + IconDevices, + IconDevices2, + IconDevicesBolt, + IconDevicesCancel, + IconDevicesCheck, + IconDevicesCode, + IconDevicesCog, + IconDevicesDollar, + IconDevicesDown, + IconDevicesExclamation, + IconDevicesHeart, + IconDevicesMinus, + IconDevicesOff, + IconDevicesPause, + IconDevicesPc, + IconDevicesPcOff, + IconDevicesPin, + IconDevicesPlus, + IconDevicesQuestion, + IconDevicesSearch, + IconDevicesShare, + IconDevicesStar, + IconDevicesUp, + IconDevicesX, + IconDiabolo, + IconDiaboloOff, + IconDiaboloPlus, + IconDialpad, + IconDialpadOff, + IconDiamond, + IconDiamondOff, + IconDiamonds, + IconDice, + IconDice1, + IconDice2, + IconDice3, + IconDice4, + IconDice5, + IconDice6, + IconDimensions, + IconDirection, + IconDirectionHorizontal, + IconDirectionSign, + IconDirectionSignOff, + IconDirections, + IconDirectionsOff, + IconDisabled, + IconDisabled2, + IconDisabledOff, + IconDisc, + IconDiscGolf, + IconDiscOff, + IconDiscount, + IconDiscountOff, + IconDivide, + IconDna, + IconDna2, + IconDna2Off, + IconDnaOff, + IconDog, + IconDogBowl, + IconDoor, + IconDoorEnter, + IconDoorExit, + IconDoorOff, + IconDots, + IconDotsCircleHorizontal, + IconDotsDiagonal, + IconDotsDiagonal2, + IconDotsVertical, + IconDownload, + IconDownloadOff, + IconDragDrop, + IconDragDrop2, + IconDrone, + IconDroneOff, + IconDropCircle, + IconDroplet, + IconDropletBolt, + IconDropletCancel, + IconDropletCheck, + IconDropletCode, + IconDropletCog, + IconDropletDollar, + IconDropletDown, + IconDropletExclamation, + IconDropletHalf, + IconDropletHalf2, + IconDropletHeart, + IconDropletMinus, + IconDropletOff, + IconDropletPause, + IconDropletPin, + IconDropletPlus, + IconDropletQuestion, + IconDropletSearch, + IconDropletShare, + IconDropletStar, + IconDropletUp, + IconDropletX, + IconDroplets, + IconDualScreen, + IconEPassport, + IconEar, + IconEarOff, + IconEaseIn, + IconEaseInControlPoint, + IconEaseInOut, + IconEaseInOutControlPoints, + IconEaseOut, + IconEaseOutControlPoint, + IconEdit, + IconEditCircle, + IconEditCircleOff, + IconEditOff, + IconEgg, + IconEggCracked, + IconEggFried, + IconEggOff, + IconEggs, + IconElevator, + IconElevatorOff, + IconEmergencyBed, + IconEmpathize, + IconEmpathizeOff, + IconEmphasis, + IconEngine, + IconEngineOff, + IconEqual, + IconEqualDouble, + IconEqualNot, + IconEraser, + IconEraserOff, + IconError404, + IconError404Off, + IconEscalator, + IconEscalatorDown, + IconEscalatorUp, + IconExchange, + IconExchangeOff, + IconExclamationCircle, + IconExclamationMark, + IconExclamationMarkOff, + IconExplicit, + IconExplicitOff, + IconExposure, + IconExposure0, + IconExposureMinus1, + IconExposureMinus2, + IconExposureOff, + IconExposurePlus1, + IconExposurePlus2, + IconExternalLink, + IconExternalLinkOff, + IconEye, + IconEyeBolt, + IconEyeCancel, + IconEyeCheck, + IconEyeClosed, + IconEyeCode, + IconEyeCog, + IconEyeDiscount, + IconEyeDollar, + IconEyeDown, + IconEyeEdit, + IconEyeExclamation, + IconEyeHeart, + IconEyeMinus, + IconEyeOff, + IconEyePause, + IconEyePin, + IconEyePlus, + IconEyeQuestion, + IconEyeSearch, + IconEyeShare, + IconEyeStar, + IconEyeTable, + IconEyeUp, + IconEyeX, + IconEyeglass, + IconEyeglass2, + IconEyeglassOff, + IconFaceId, + IconFaceIdError, + IconFaceMask, + IconFaceMaskOff, + IconFall, + IconFeather, + IconFeatherOff, + IconFence, + IconFenceOff, + IconFidgetSpinner, + IconFile, + IconFile3d, + IconFileAlert, + IconFileAnalytics, + IconFileArrowLeft, + IconFileArrowRight, + IconFileBarcode, + IconFileBroken, + IconFileCertificate, + IconFileChart, + IconFileCheck, + IconFileCode, + IconFileCode2, + IconFileCv, + IconFileDatabase, + IconFileDelta, + IconFileDescription, + IconFileDiff, + IconFileDigit, + IconFileDislike, + IconFileDollar, + IconFileDots, + IconFileDownload, + IconFileEuro, + IconFileExport, + IconFileFunction, + IconFileHorizontal, + IconFileImport, + IconFileInfinity, + IconFileInfo, + IconFileInvoice, + IconFileLambda, + IconFileLike, + IconFileMinus, + IconFileMusic, + IconFileOff, + IconFileOrientation, + IconFilePencil, + IconFilePercent, + IconFilePhone, + IconFilePlus, + IconFilePower, + IconFileReport, + IconFileRss, + IconFileScissors, + IconFileSearch, + IconFileSettings, + IconFileShredder, + IconFileSignal, + IconFileSpreadsheet, + IconFileStack, + IconFileStar, + IconFileSymlink, + IconFileText, + IconFileTextAi, + IconFileTime, + IconFileTypeBmp, + IconFileTypeCss, + IconFileTypeCsv, + IconFileTypeDoc, + IconFileTypeDocx, + IconFileTypeHtml, + IconFileTypeJpg, + IconFileTypeJs, + IconFileTypeJsx, + IconFileTypePdf, + IconFileTypePhp, + IconFileTypePng, + IconFileTypePpt, + IconFileTypeRs, + IconFileTypeSql, + IconFileTypeSvg, + IconFileTypeTs, + IconFileTypeTsx, + IconFileTypeTxt, + IconFileTypeVue, + IconFileTypeXls, + IconFileTypeXml, + IconFileTypeZip, + IconFileTypography, + IconFileUnknown, + IconFileUpload, + IconFileVector, + IconFileX, + IconFileZip, + IconFiles, + IconFilesOff, + IconFilter, + IconFilterBolt, + IconFilterCancel, + IconFilterCheck, + IconFilterCode, + IconFilterCog, + IconFilterDiscount, + IconFilterDollar, + IconFilterDown, + IconFilterEdit, + IconFilterExclamation, + IconFilterHeart, + IconFilterMinus, + IconFilterOff, + IconFilterPause, + IconFilterPin, + IconFilterPlus, + IconFilterQuestion, + IconFilterSearch, + IconFilterShare, + IconFilterStar, + IconFilterUp, + IconFilterX, + IconFilters, + IconFingerprint, + IconFingerprintOff, + IconFireExtinguisher, + IconFireHydrant, + IconFireHydrantOff, + IconFiretruck, + IconFirstAidKit, + IconFirstAidKitOff, + IconFish, + IconFishBone, + IconFishChristianity, + IconFishHook, + IconFishHookOff, + IconFishOff, + IconFlag, + IconFlag2, + IconFlag2Off, + IconFlag3, + IconFlagBolt, + IconFlagCancel, + IconFlagCheck, + IconFlagCode, + IconFlagCog, + IconFlagDiscount, + IconFlagDollar, + IconFlagDown, + IconFlagExclamation, + IconFlagHeart, + IconFlagMinus, + IconFlagOff, + IconFlagPause, + IconFlagPin, + IconFlagPlus, + IconFlagQuestion, + IconFlagSearch, + IconFlagShare, + IconFlagStar, + IconFlagUp, + IconFlagX, + IconFlame, + IconFlameOff, + IconFlare, + IconFlask, + IconFlask2, + IconFlask2Off, + IconFlaskOff, + IconFlipFlops, + IconFlipHorizontal, + IconFlipVertical, + IconFloatCenter, + IconFloatLeft, + IconFloatNone, + IconFloatRight, + IconFlower, + IconFlowerOff, + IconFocus, + IconFocus2, + IconFocusAuto, + IconFocusCentered, + IconFold, + IconFoldDown, + IconFoldUp, + IconFolder, + IconFolderBolt, + IconFolderCancel, + IconFolderCheck, + IconFolderCode, + IconFolderCog, + IconFolderDollar, + IconFolderDown, + IconFolderExclamation, + IconFolderHeart, + IconFolderMinus, + IconFolderOff, + IconFolderOpen, + IconFolderPause, + IconFolderPin, + IconFolderPlus, + IconFolderQuestion, + IconFolderSearch, + IconFolderShare, + IconFolderStar, + IconFolderSymlink, + IconFolderUp, + IconFolderX, + IconFolders, + IconFoldersOff, + IconForbid, + IconForbid2, + IconForklift, + IconForms, + IconFountain, + IconFountainOff, + IconFrame, + IconFrameOff, + IconFreeRights, + IconFreezeColumn, + IconFreezeRow, + IconFreezeRowColumn, + IconFridge, + IconFridgeOff, + IconFriends, + IconFriendsOff, + IconFrustum, + IconFrustumOff, + IconFrustumPlus, + IconFunction, + IconFunctionOff, + IconGardenCart, + IconGardenCartOff, + IconGasStation, + IconGasStationOff, + IconGauge, + IconGaugeOff, + IconGavel, + IconGenderAgender, + IconGenderAndrogyne, + IconGenderBigender, + IconGenderDemiboy, + IconGenderDemigirl, + IconGenderEpicene, + IconGenderFemale, + IconGenderFemme, + IconGenderGenderfluid, + IconGenderGenderless, + IconGenderGenderqueer, + IconGenderHermaphrodite, + IconGenderIntergender, + IconGenderMale, + IconGenderNeutrois, + IconGenderThird, + IconGenderTransgender, + IconGenderTrasvesti, + IconGeometry, + IconGhost, + IconGhost2, + IconGhost3, + IconGhostOff, + IconGif, + IconGift, + IconGiftCard, + IconGiftOff, + IconGitBranch, + IconGitBranchDeleted, + IconGitCherryPick, + IconGitCommit, + IconGitCompare, + IconGitFork, + IconGitMerge, + IconGitPullRequest, + IconGitPullRequestClosed, + IconGitPullRequestDraft, + IconGizmo, + IconGlass, + IconGlassFull, + IconGlassOff, + IconGlobe, + IconGlobeOff, + IconGoGame, + IconGolf, + IconGolfOff, + IconGps, + IconGradienter, + IconGrain, + IconGraph, + IconGraphOff, + IconGrave, + IconGrave2, + IconGridDots, + IconGridPattern, + IconGrill, + IconGrillFork, + IconGrillOff, + IconGrillSpatula, + IconGripHorizontal, + IconGripVertical, + IconGrowth, + IconGuitarPick, + IconH1, + IconH2, + IconH3, + IconH4, + IconH5, + IconH6, + IconHammer, + IconHammerOff, + IconHandClick, + IconHandFinger, + IconHandFingerOff, + IconHandGrab, + IconHandLittleFinger, + IconHandMiddleFinger, + IconHandMove, + IconHandOff, + IconHandRingFinger, + IconHandSanitizer, + IconHandStop, + IconHandThreeFingers, + IconHandTwoFingers, + IconHanger, + IconHanger2, + IconHangerOff, + IconHash, + IconHaze, + IconHazeMoon, + IconHdr, + IconHeading, + IconHeadingOff, + IconHeadphones, + IconHeadphonesOff, + IconHeadset, + IconHeadsetOff, + IconHealthRecognition, + IconHeart, + IconHeartBolt, + IconHeartBroken, + IconHeartCancel, + IconHeartCheck, + IconHeartCode, + IconHeartCog, + IconHeartDiscount, + IconHeartDollar, + IconHeartDown, + IconHeartExclamation, + IconHeartHandshake, + IconHeartMinus, + IconHeartOff, + IconHeartPause, + IconHeartPin, + IconHeartPlus, + IconHeartQuestion, + IconHeartRateMonitor, + IconHeartSearch, + IconHeartShare, + IconHeartStar, + IconHeartUp, + IconHeartX, + IconHeartbeat, + IconHearts, + IconHeartsOff, + IconHelicopter, + IconHelicopterLanding, + IconHelmet, + IconHelmetOff, + IconHelp, + IconHelpCircle, + IconHelpHexagon, + IconHelpOctagon, + IconHelpOff, + IconHelpSmall, + IconHelpSquare, + IconHelpSquareRounded, + IconHelpTriangle, + IconHemisphere, + IconHemisphereOff, + IconHemispherePlus, + IconHexagon, + IconHexagon3d, + IconHexagonLetterA, + IconHexagonLetterB, + IconHexagonLetterC, + IconHexagonLetterD, + IconHexagonLetterE, + IconHexagonLetterF, + IconHexagonLetterG, + IconHexagonLetterH, + IconHexagonLetterI, + IconHexagonLetterJ, + IconHexagonLetterK, + IconHexagonLetterL, + IconHexagonLetterM, + IconHexagonLetterN, + IconHexagonLetterO, + IconHexagonLetterP, + IconHexagonLetterQ, + IconHexagonLetterR, + IconHexagonLetterS, + IconHexagonLetterT, + IconHexagonLetterU, + IconHexagonLetterV, + IconHexagonLetterW, + IconHexagonLetterX, + IconHexagonLetterY, + IconHexagonLetterZ, + IconHexagonNumber0, + IconHexagonNumber1, + IconHexagonNumber2, + IconHexagonNumber3, + IconHexagonNumber4, + IconHexagonNumber5, + IconHexagonNumber6, + IconHexagonNumber7, + IconHexagonNumber8, + IconHexagonNumber9, + IconHexagonOff, + IconHexagonalPrism, + IconHexagonalPrismOff, + IconHexagonalPrismPlus, + IconHexagonalPyramid, + IconHexagonalPyramidOff, + IconHexagonalPyramidPlus, + IconHexagons, + IconHexagonsOff, + IconHierarchy, + IconHierarchy2, + IconHierarchy3, + IconHierarchyOff, + IconHighlight, + IconHighlightOff, + IconHistory, + IconHistoryOff, + IconHistoryToggle, + IconHome, + IconHome2, + IconHomeBolt, + IconHomeCancel, + IconHomeCheck, + IconHomeCog, + IconHomeDollar, + IconHomeDot, + IconHomeDown, + IconHomeEco, + IconHomeEdit, + IconHomeExclamation, + IconHomeHand, + IconHomeHeart, + IconHomeInfinity, + IconHomeLink, + IconHomeMinus, + IconHomeMove, + IconHomeOff, + IconHomePlus, + IconHomeQuestion, + IconHomeRibbon, + IconHomeSearch, + IconHomeShare, + IconHomeShield, + IconHomeSignal, + IconHomeStar, + IconHomeStats, + IconHomeUp, + IconHomeX, + IconHorseToy, + IconHotelService, + IconHourglass, + IconHourglassEmpty, + IconHourglassHigh, + IconHourglassLow, + IconHourglassOff, + IconHours24, + IconHtml, + IconHttpConnect, + IconHttpDelete, + IconHttpGet, + IconHttpHead, + IconHttpOptions, + IconHttpPatch, + IconHttpPost, + IconHttpPut, + IconHttpQue, + IconHttpTrace, + IconIceCream, + IconIceCream2, + IconIceCreamOff, + IconIceSkating, + IconIcons, + IconIconsOff, + IconId, + IconIdBadge, + IconIdBadge2, + IconIdBadgeOff, + IconIdOff, + IconInbox, + IconInboxOff, + IconIndentDecrease, + IconIndentIncrease, + IconInfinity, + IconInfinityOff, + IconInfoCircle, + IconInfoHexagon, + IconInfoOctagon, + IconInfoSmall, + IconInfoSquare, + IconInfoSquareRounded, + IconInfoTriangle, + IconInnerShadowBottom, + IconInnerShadowBottomLeft, + IconInnerShadowBottomRight, + IconInnerShadowLeft, + IconInnerShadowRight, + IconInnerShadowTop, + IconInnerShadowTopLeft, + IconInnerShadowTopRight, + IconInputSearch, + IconIroning, + IconIroning1, + IconIroning2, + IconIroning3, + IconIroningOff, + IconIroningSteam, + IconIroningSteamOff, + IconIrregularPolyhedron, + IconIrregularPolyhedronOff, + IconIrregularPolyhedronPlus, + IconItalic, + IconJacket, + IconJetpack, + IconJewishStar, + IconJpg, + IconJson, + IconJumpRope, + IconKarate, + IconKayak, + IconKey, + IconKeyOff, + IconKeyboard, + IconKeyboardHide, + IconKeyboardOff, + IconKeyboardShow, + IconKeyframe, + IconKeyframeAlignCenter, + IconKeyframeAlignHorizontal, + IconKeyframeAlignVertical, + IconKeyframes, + IconLadder, + IconLadderOff, + IconLadle, + IconLambda, + IconLamp, + IconLamp2, + IconLampOff, + IconLane, + IconLanguage, + IconLanguageHiragana, + IconLanguageKatakana, + IconLanguageOff, + IconLasso, + IconLassoOff, + IconLassoPolygon, + IconLayersDifference, + IconLayersIntersect, + IconLayersIntersect2, + IconLayersLinked, + IconLayersOff, + IconLayersSubtract, + IconLayersUnion, + IconLayout, + IconLayout2, + IconLayoutAlignBottom, + IconLayoutAlignCenter, + IconLayoutAlignLeft, + IconLayoutAlignMiddle, + IconLayoutAlignRight, + IconLayoutAlignTop, + IconLayoutBoard, + IconLayoutBoardSplit, + IconLayoutBottombar, + IconLayoutBottombarCollapse, + IconLayoutBottombarExpand, + IconLayoutCards, + IconLayoutCollage, + IconLayoutColumns, + IconLayoutDashboard, + IconLayoutDistributeHorizontal, + IconLayoutDistributeVertical, + IconLayoutGrid, + IconLayoutGridAdd, + IconLayoutGridRemove, + IconLayoutKanban, + IconLayoutList, + IconLayoutNavbar, + IconLayoutNavbarCollapse, + IconLayoutNavbarExpand, + IconLayoutOff, + IconLayoutRows, + IconLayoutSidebar, + IconLayoutSidebarLeftCollapse, + IconLayoutSidebarLeftExpand, + IconLayoutSidebarRight, + IconLayoutSidebarRightCollapse, + IconLayoutSidebarRightExpand, + IconLeaf, + IconLeafOff, + IconLego, + IconLegoOff, + IconLemon, + IconLemon2, + IconLetterA, + IconLetterB, + IconLetterC, + IconLetterCase, + IconLetterCaseLower, + IconLetterCaseToggle, + IconLetterCaseUpper, + IconLetterD, + IconLetterE, + IconLetterF, + IconLetterG, + IconLetterH, + IconLetterI, + IconLetterJ, + IconLetterK, + IconLetterL, + IconLetterM, + IconLetterN, + IconLetterO, + IconLetterP, + IconLetterQ, + IconLetterR, + IconLetterS, + IconLetterSpacing, + IconLetterT, + IconLetterU, + IconLetterV, + IconLetterW, + IconLetterX, + IconLetterY, + IconLetterZ, + IconLicense, + IconLicenseOff, + IconLifebuoy, + IconLifebuoyOff, + IconLighter, + IconLine, + IconLineDashed, + IconLineDotted, + IconLineHeight, + IconLink, + IconLinkOff, + IconLinkPlus, + IconList, + IconListCheck, + IconListDetails, + IconListNumbers, + IconListSearch, + IconListTree, + IconLivePhoto, + IconLivePhotoOff, + IconLiveView, + IconLoadBalancer, + IconLoader, + IconLoader2, + IconLoader3, + IconLoaderQuarter, + IconLocation, + IconLocationBolt, + IconLocationBroken, + IconLocationCancel, + IconLocationCheck, + IconLocationCode, + IconLocationCog, + IconLocationDiscount, + IconLocationDollar, + IconLocationDown, + IconLocationExclamation, + IconLocationHeart, + IconLocationMinus, + IconLocationOff, + IconLocationPause, + IconLocationPin, + IconLocationPlus, + IconLocationQuestion, + IconLocationSearch, + IconLocationShare, + IconLocationStar, + IconLocationUp, + IconLocationX, + IconLock, + IconLockAccess, + IconLockAccessOff, + IconLockBolt, + IconLockCancel, + IconLockCheck, + IconLockCode, + IconLockCog, + IconLockDollar, + IconLockDown, + IconLockExclamation, + IconLockHeart, + IconLockMinus, + IconLockOff, + IconLockOpen, + IconLockOpenOff, + IconLockPause, + IconLockPin, + IconLockPlus, + IconLockQuestion, + IconLockSearch, + IconLockShare, + IconLockSquare, + IconLockSquareRounded, + IconLockStar, + IconLockUp, + IconLockX, + IconLogicAnd, + IconLogicBuffer, + IconLogicNand, + IconLogicNor, + IconLogicNot, + IconLogicOr, + IconLogicXnor, + IconLogicXor, + IconLogin, + IconLogout, + IconLogout2, + IconLollipop, + IconLollipopOff, + IconLuggage, + IconLuggageOff, + IconLungs, + IconLungsOff, + IconMacro, + IconMacroOff, + IconMagnet, + IconMagnetOff, + IconMail, + IconMailAi, + IconMailBolt, + IconMailCancel, + IconMailCheck, + IconMailCode, + IconMailCog, + IconMailDollar, + IconMailDown, + IconMailExclamation, + IconMailFast, + IconMailForward, + IconMailHeart, + IconMailMinus, + IconMailOff, + IconMailOpened, + IconMailPause, + IconMailPin, + IconMailPlus, + IconMailQuestion, + IconMailSearch, + IconMailShare, + IconMailStar, + IconMailUp, + IconMailX, + IconMailbox, + IconMailboxOff, + IconMan, + IconManualGearbox, + IconMap, + IconMap2, + IconMapBolt, + IconMapCancel, + IconMapCheck, + IconMapCode, + IconMapCog, + IconMapDiscount, + IconMapDollar, + IconMapDown, + IconMapExclamation, + IconMapHeart, + IconMapMinus, + IconMapOff, + IconMapPause, + IconMapPin, + IconMapPinBolt, + IconMapPinCancel, + IconMapPinCheck, + IconMapPinCode, + IconMapPinCog, + IconMapPinDollar, + IconMapPinDown, + IconMapPinExclamation, + IconMapPinHeart, + IconMapPinMinus, + IconMapPinOff, + IconMapPinPause, + IconMapPinPin, + IconMapPinPlus, + IconMapPinQuestion, + IconMapPinSearch, + IconMapPinShare, + IconMapPinStar, + IconMapPinUp, + IconMapPinX, + IconMapPins, + IconMapPlus, + IconMapQuestion, + IconMapSearch, + IconMapShare, + IconMapStar, + IconMapUp, + IconMapX, + IconMarkdown, + IconMarkdownOff, + IconMarquee, + IconMarquee2, + IconMarqueeOff, + IconMars, + IconMask, + IconMaskOff, + IconMasksTheater, + IconMasksTheaterOff, + IconMassage, + IconMatchstick, + IconMath, + IconMath1Divide2, + IconMath1Divide3, + IconMathAvg, + IconMathEqualGreater, + IconMathEqualLower, + IconMathFunction, + IconMathFunctionOff, + IconMathFunctionY, + IconMathGreater, + IconMathIntegral, + IconMathIntegralX, + IconMathIntegrals, + IconMathLower, + IconMathMax, + IconMathMin, + IconMathNot, + IconMathOff, + IconMathPi, + IconMathPiDivide2, + IconMathSymbols, + IconMathXDivide2, + IconMathXDivideY, + IconMathXDivideY2, + IconMathXMinusX, + IconMathXMinusY, + IconMathXPlusX, + IconMathXPlusY, + IconMathXy, + IconMathYMinusY, + IconMathYPlusY, + IconMaximize, + IconMaximizeOff, + IconMeat, + IconMeatOff, + IconMedal, + IconMedal2, + IconMedicalCross, + IconMedicalCrossCircle, + IconMedicalCrossOff, + IconMedicineSyrup, + IconMeeple, + IconMenorah, + IconMenu, + IconMenu2, + IconMenuDeep, + IconMenuOrder, + IconMessage, + IconMessage2, + IconMessage2Bolt, + IconMessage2Cancel, + IconMessage2Check, + IconMessage2Code, + IconMessage2Cog, + IconMessage2Dollar, + IconMessage2Down, + IconMessage2Exclamation, + IconMessage2Heart, + IconMessage2Minus, + IconMessage2Off, + IconMessage2Pause, + IconMessage2Pin, + IconMessage2Plus, + IconMessage2Question, + IconMessage2Search, + IconMessage2Share, + IconMessage2Star, + IconMessage2Up, + IconMessage2X, + IconMessageBolt, + IconMessageCancel, + IconMessageChatbot, + IconMessageCheck, + IconMessageCircle, + IconMessageCircleBolt, + IconMessageCircleCancel, + IconMessageCircleCheck, + IconMessageCircleCode, + IconMessageCircleCog, + IconMessageCircleDollar, + IconMessageCircleDown, + IconMessageCircleExclamation, + IconMessageCircleHeart, + IconMessageCircleMinus, + IconMessageCircleOff, + IconMessageCirclePause, + IconMessageCirclePin, + IconMessageCirclePlus, + IconMessageCircleQuestion, + IconMessageCircleSearch, + IconMessageCircleShare, + IconMessageCircleStar, + IconMessageCircleUp, + IconMessageCircleX, + IconMessageCode, + IconMessageCog, + IconMessageDollar, + IconMessageDots, + IconMessageDown, + IconMessageExclamation, + IconMessageForward, + IconMessageHeart, + IconMessageLanguage, + IconMessageMinus, + IconMessageOff, + IconMessagePause, + IconMessagePin, + IconMessagePlus, + IconMessageQuestion, + IconMessageReport, + IconMessageSearch, + IconMessageShare, + IconMessageStar, + IconMessageUp, + IconMessageX, + IconMessages, + IconMessagesOff, + IconMeteor, + IconMeteorOff, + IconMichelinBibGourmand, + IconMichelinStar, + IconMichelinStarGreen, + IconMickey, + IconMicrophone, + IconMicrophone2, + IconMicrophone2Off, + IconMicrophoneOff, + IconMicroscope, + IconMicroscopeOff, + IconMicrowave, + IconMicrowaveOff, + IconMilitaryAward, + IconMilitaryRank, + IconMilk, + IconMilkOff, + IconMilkshake, + IconMinimize, + IconMinus, + IconMist, + IconMistOff, + IconMobiledata, + IconMobiledataOff, + IconMoneybag, + IconMoodAngry, + IconMoodAnnoyed, + IconMoodAnnoyed2, + IconMoodBoy, + IconMoodCheck, + IconMoodCog, + IconMoodConfuzed, + IconMoodCrazyHappy, + IconMoodCry, + IconMoodDollar, + IconMoodEdit, + IconMoodEmpty, + IconMoodHappy, + IconMoodHeart, + IconMoodKid, + IconMoodLookLeft, + IconMoodLookRight, + IconMoodMinus, + IconMoodNerd, + IconMoodNervous, + IconMoodNeutral, + IconMoodOff, + IconMoodPin, + IconMoodPlus, + IconMoodSad, + IconMoodSad2, + IconMoodSadDizzy, + IconMoodSadSquint, + IconMoodSearch, + IconMoodShare, + IconMoodSick, + IconMoodSilence, + IconMoodSing, + IconMoodSmile, + IconMoodSmileBeam, + IconMoodSmileDizzy, + IconMoodTongue, + IconMoodTongueWink, + IconMoodTongueWink2, + IconMoodUnamused, + IconMoodUp, + IconMoodWink, + IconMoodWink2, + IconMoodWrrr, + IconMoodX, + IconMoodXd, + IconMoon, + IconMoon2, + IconMoonOff, + IconMoonStars, + IconMoped, + IconMotorbike, + IconMountain, + IconMountainOff, + IconMouse, + IconMouse2, + IconMouseOff, + IconMoustache, + IconMovie, + IconMovieOff, + IconMug, + IconMugOff, + IconMultiplier05x, + IconMultiplier15x, + IconMultiplier1x, + IconMultiplier2x, + IconMushroom, + IconMushroomOff, + IconMusic, + IconMusicBolt, + IconMusicCancel, + IconMusicCheck, + IconMusicCode, + IconMusicCog, + IconMusicDiscount, + IconMusicDollar, + IconMusicDown, + IconMusicExclamation, + IconMusicHeart, + IconMusicMinus, + IconMusicOff, + IconMusicPause, + IconMusicPin, + IconMusicPlus, + IconMusicQuestion, + IconMusicSearch, + IconMusicShare, + IconMusicStar, + IconMusicUp, + IconMusicX, + IconNavigation, + IconNavigationBolt, + IconNavigationCancel, + IconNavigationCheck, + IconNavigationCode, + IconNavigationCog, + IconNavigationDiscount, + IconNavigationDollar, + IconNavigationDown, + IconNavigationExclamation, + IconNavigationHeart, + IconNavigationMinus, + IconNavigationNorth, + IconNavigationOff, + IconNavigationPause, + IconNavigationPin, + IconNavigationPlus, + IconNavigationQuestion, + IconNavigationSearch, + IconNavigationShare, + IconNavigationStar, + IconNavigationUp, + IconNavigationX, + IconNeedle, + IconNeedleThread, + IconNetwork, + IconNetworkOff, + IconNewSection, + IconNews, + IconNewsOff, + IconNfc, + IconNfcOff, + IconNoCopyright, + IconNoCreativeCommons, + IconNoDerivatives, + IconNorthStar, + IconNote, + IconNoteOff, + IconNotebook, + IconNotebookOff, + IconNotes, + IconNotesOff, + IconNotification, + IconNotificationOff, + IconNumber, + IconNumber0, + IconNumber1, + IconNumber2, + IconNumber3, + IconNumber4, + IconNumber5, + IconNumber6, + IconNumber7, + IconNumber8, + IconNumber9, + IconNumbers, + IconNurse, + IconOctagon, + IconOctagonOff, + IconOctahedron, + IconOctahedronOff, + IconOctahedronPlus, + IconOld, + IconOlympics, + IconOlympicsOff, + IconOm, + IconOmega, + IconOutbound, + IconOutlet, + IconOval, + IconOvalVertical, + IconOverline, + IconPackage, + IconPackageExport, + IconPackageImport, + IconPackageOff, + IconPackages, + IconPacman, + IconPageBreak, + IconPaint, + IconPaintOff, + IconPalette, + IconPaletteOff, + IconPanoramaHorizontal, + IconPanoramaHorizontalOff, + IconPanoramaVertical, + IconPanoramaVerticalOff, + IconPaperBag, + IconPaperBagOff, + IconPaperclip, + IconParachute, + IconParachuteOff, + IconParentheses, + IconParenthesesOff, + IconParking, + IconParkingOff, + IconPassword, + IconPaw, + IconPawOff, + IconPdf, + IconPeace, + IconPencil, + IconPencilBolt, + IconPencilCancel, + IconPencilCheck, + IconPencilCode, + IconPencilCog, + IconPencilDiscount, + IconPencilDollar, + IconPencilDown, + IconPencilExclamation, + IconPencilHeart, + IconPencilMinus, + IconPencilOff, + IconPencilPause, + IconPencilPin, + IconPencilPlus, + IconPencilQuestion, + IconPencilSearch, + IconPencilShare, + IconPencilStar, + IconPencilUp, + IconPencilX, + IconPennant, + IconPennant2, + IconPennantOff, + IconPentagon, + IconPentagonOff, + IconPentagram, + IconPepper, + IconPepperOff, + IconPercentage, + IconPerfume, + IconPerspective, + IconPerspectiveOff, + IconPhone, + IconPhoneCall, + IconPhoneCalling, + IconPhoneCheck, + IconPhoneIncoming, + IconPhoneOff, + IconPhoneOutgoing, + IconPhonePause, + IconPhonePlus, + IconPhoneX, + IconPhoto, + IconPhotoAi, + IconPhotoBolt, + IconPhotoCancel, + IconPhotoCheck, + IconPhotoCode, + IconPhotoCog, + IconPhotoDollar, + IconPhotoDown, + IconPhotoEdit, + IconPhotoExclamation, + IconPhotoHeart, + IconPhotoMinus, + IconPhotoOff, + IconPhotoPause, + IconPhotoPin, + IconPhotoPlus, + IconPhotoQuestion, + IconPhotoSearch, + IconPhotoSensor, + IconPhotoSensor2, + IconPhotoSensor3, + IconPhotoShare, + IconPhotoShield, + IconPhotoStar, + IconPhotoUp, + IconPhotoX, + IconPhysotherapist, + IconPiano, + IconPick, + IconPictureInPicture, + IconPictureInPictureOff, + IconPictureInPictureOn, + IconPictureInPictureTop, + IconPig, + IconPigMoney, + IconPigOff, + IconPilcrow, + IconPill, + IconPillOff, + IconPills, + IconPin, + IconPingPong, + IconPinned, + IconPinnedOff, + IconPizza, + IconPizzaOff, + IconPlaceholder, + IconPlane, + IconPlaneArrival, + IconPlaneDeparture, + IconPlaneInflight, + IconPlaneOff, + IconPlaneTilt, + IconPlanet, + IconPlanetOff, + IconPlant, + IconPlant2, + IconPlant2Off, + IconPlantOff, + IconPlayBasketball, + IconPlayCard, + IconPlayCardOff, + IconPlayFootball, + IconPlayHandball, + IconPlayVolleyball, + IconPlayerEject, + IconPlayerPause, + IconPlayerPlay, + IconPlayerRecord, + IconPlayerSkipBack, + IconPlayerSkipForward, + IconPlayerStop, + IconPlayerTrackNext, + IconPlayerTrackPrev, + IconPlaylist, + IconPlaylistAdd, + IconPlaylistOff, + IconPlaylistX, + IconPlaystationCircle, + IconPlaystationSquare, + IconPlaystationTriangle, + IconPlaystationX, + IconPlug, + IconPlugConnected, + IconPlugConnectedX, + IconPlugOff, + IconPlugX, + IconPlus, + IconPlusEqual, + IconPlusMinus, + IconPng, + IconPodium, + IconPodiumOff, + IconPoint, + IconPointOff, + IconPointer, + IconPointerBolt, + IconPointerCancel, + IconPointerCheck, + IconPointerCode, + IconPointerCog, + IconPointerDollar, + IconPointerDown, + IconPointerExclamation, + IconPointerHeart, + IconPointerMinus, + IconPointerOff, + IconPointerPause, + IconPointerPin, + IconPointerPlus, + IconPointerQuestion, + IconPointerSearch, + IconPointerShare, + IconPointerStar, + IconPointerUp, + IconPointerX, + IconPokeball, + IconPokeballOff, + IconPokerChip, + IconPolaroid, + IconPolygon, + IconPolygonOff, + IconPoo, + IconPool, + IconPoolOff, + IconPower, + IconPray, + IconPremiumRights, + IconPrescription, + IconPresentation, + IconPresentationAnalytics, + IconPresentationOff, + IconPrinter, + IconPrinterOff, + IconPrism, + IconPrismOff, + IconPrismPlus, + IconPrison, + IconProgress, + IconProgressAlert, + IconProgressBolt, + IconProgressCheck, + IconProgressDown, + IconProgressHelp, + IconProgressX, + IconPrompt, + IconPropeller, + IconPropellerOff, + IconPumpkinScary, + IconPuzzle, + IconPuzzle2, + IconPuzzleOff, + IconPyramid, + IconPyramidOff, + IconPyramidPlus, + IconQrcode, + IconQrcodeOff, + IconQuestionMark, + IconQuote, + IconQuoteOff, + IconQuotes, + IconRadar, + IconRadar2, + IconRadarOff, + IconRadio, + IconRadioOff, + IconRadioactive, + IconRadioactiveOff, + IconRadiusBottomLeft, + IconRadiusBottomRight, + IconRadiusTopLeft, + IconRadiusTopRight, + IconRainbow, + IconRainbowOff, + IconRating12Plus, + IconRating14Plus, + IconRating16Plus, + IconRating18Plus, + IconRating21Plus, + IconRazor, + IconRazorElectric, + IconReceipt, + IconReceipt2, + IconReceiptOff, + IconReceiptRefund, + IconReceiptTax, + IconRecharging, + IconRecordMail, + IconRecordMailOff, + IconRectangle, + IconRectangleRoundedBottom, + IconRectangleRoundedTop, + IconRectangleVertical, + IconRectangularPrism, + IconRectangularPrismOff, + IconRectangularPrismPlus, + IconRecycle, + IconRecycleOff, + IconRefresh, + IconRefreshAlert, + IconRefreshDot, + IconRefreshOff, + IconRegex, + IconRegexOff, + IconRegistered, + IconRelationManyToMany, + IconRelationOneToMany, + IconRelationOneToOne, + IconReload, + IconReorder, + IconRepeat, + IconRepeatOff, + IconRepeatOnce, + IconReplace, + IconReplaceOff, + IconReport, + IconReportAnalytics, + IconReportMedical, + IconReportMoney, + IconReportOff, + IconReportSearch, + IconReservedLine, + IconResize, + IconRestore, + IconRewindBackward10, + IconRewindBackward15, + IconRewindBackward20, + IconRewindBackward30, + IconRewindBackward40, + IconRewindBackward5, + IconRewindBackward50, + IconRewindBackward60, + IconRewindForward10, + IconRewindForward15, + IconRewindForward20, + IconRewindForward30, + IconRewindForward40, + IconRewindForward5, + IconRewindForward50, + IconRewindForward60, + IconRibbonHealth, + IconRings, + IconRipple, + IconRippleOff, + IconRoad, + IconRoadOff, + IconRoadSign, + IconRobot, + IconRobotOff, + IconRocket, + IconRocketOff, + IconRollerSkating, + IconRollercoaster, + IconRollercoasterOff, + IconRosette, + IconRosetteNumber0, + IconRosetteNumber1, + IconRosetteNumber2, + IconRosetteNumber3, + IconRosetteNumber4, + IconRosetteNumber5, + IconRosetteNumber6, + IconRosetteNumber7, + IconRosetteNumber8, + IconRosetteNumber9, + IconRotate, + IconRotate2, + IconRotate360, + IconRotate3d, + IconRotateClockwise, + IconRotateClockwise2, + IconRotateDot, + IconRotateRectangle, + IconRoute, + IconRoute2, + IconRouteOff, + IconRouter, + IconRouterOff, + IconRowInsertBottom, + IconRowInsertTop, + IconRowRemove, + IconRss, + IconRubberStamp, + IconRubberStampOff, + IconRuler, + IconRuler2, + IconRuler2Off, + IconRuler3, + IconRulerMeasure, + IconRulerOff, + IconRun, + IconSTurnDown, + IconSTurnLeft, + IconSTurnRight, + IconSTurnUp, + IconSailboat, + IconSailboat2, + IconSailboatOff, + IconSalad, + IconSalt, + IconSatellite, + IconSatelliteOff, + IconSausage, + IconScale, + IconScaleOff, + IconScaleOutline, + IconScaleOutlineOff, + IconScan, + IconScanEye, + IconSchema, + IconSchemaOff, + IconSchool, + IconSchoolBell, + IconSchoolOff, + IconScissors, + IconScissorsOff, + IconScooter, + IconScooterElectric, + IconScoreboard, + IconScreenShare, + IconScreenShareOff, + IconScreenshot, + IconScribble, + IconScribbleOff, + IconScript, + IconScriptMinus, + IconScriptPlus, + IconScriptX, + IconScubaMask, + IconScubaMaskOff, + IconSdk, + IconSearch, + IconSearchOff, + IconSection, + IconSectionSign, + IconSelect, + IconSelectAll, + IconSelector, + IconSend, + IconSendOff, + IconSeo, + IconSeparator, + IconSeparatorHorizontal, + IconSeparatorVertical, + IconServer, + IconServer2, + IconServerBolt, + IconServerCog, + IconServerOff, + IconServicemark, + IconSettings, + IconSettings2, + IconSettingsAutomation, + IconSettingsBolt, + IconSettingsCancel, + IconSettingsCheck, + IconSettingsCode, + IconSettingsCog, + IconSettingsDollar, + IconSettingsDown, + IconSettingsExclamation, + IconSettingsHeart, + IconSettingsMinus, + IconSettingsOff, + IconSettingsPause, + IconSettingsPin, + IconSettingsPlus, + IconSettingsQuestion, + IconSettingsSearch, + IconSettingsShare, + IconSettingsStar, + IconSettingsUp, + IconSettingsX, + IconShadow, + IconShadowOff, + IconShape, + IconShape2, + IconShape3, + IconShapeOff, + IconShare, + IconShare2, + IconShare3, + IconShareOff, + IconShield, + IconShieldBolt, + IconShieldCancel, + IconShieldCheck, + IconShieldCheckered, + IconShieldChevron, + IconShieldCode, + IconShieldCog, + IconShieldDollar, + IconShieldDown, + IconShieldExclamation, + IconShieldHalf, + IconShieldHeart, + IconShieldLock, + IconShieldMinus, + IconShieldOff, + IconShieldPause, + IconShieldPin, + IconShieldPlus, + IconShieldQuestion, + IconShieldSearch, + IconShieldShare, + IconShieldStar, + IconShieldUp, + IconShieldX, + IconShip, + IconShipOff, + IconShirt, + IconShirtOff, + IconShirtSport, + IconShoe, + IconShoeOff, + IconShoppingBag, + IconShoppingBagCheck, + IconShoppingBagDiscount, + IconShoppingBagEdit, + IconShoppingBagExclamation, + IconShoppingBagMinus, + IconShoppingBagPlus, + IconShoppingBagSearch, + IconShoppingBagX, + IconShoppingCart, + IconShoppingCartBolt, + IconShoppingCartCancel, + IconShoppingCartCheck, + IconShoppingCartCode, + IconShoppingCartCog, + IconShoppingCartCopy, + IconShoppingCartDiscount, + IconShoppingCartDollar, + IconShoppingCartDown, + IconShoppingCartExclamation, + IconShoppingCartHeart, + IconShoppingCartMinus, + IconShoppingCartOff, + IconShoppingCartPause, + IconShoppingCartPin, + IconShoppingCartPlus, + IconShoppingCartQuestion, + IconShoppingCartSearch, + IconShoppingCartShare, + IconShoppingCartStar, + IconShoppingCartUp, + IconShoppingCartX, + IconShovel, + IconShredder, + IconSignLeft, + IconSignRight, + IconSignal2g, + IconSignal3g, + IconSignal4g, + IconSignal4gPlus, + IconSignal5g, + IconSignal6g, + IconSignalE, + IconSignalG, + IconSignalH, + IconSignalHPlus, + IconSignalLte, + IconSignature, + IconSignatureOff, + IconSitemap, + IconSitemapOff, + IconSkateboard, + IconSkateboardOff, + IconSkateboarding, + IconSkull, + IconSlash, + IconSlashes, + IconSleigh, + IconSlice, + IconSlideshow, + IconSmartHome, + IconSmartHomeOff, + IconSmoking, + IconSmokingNo, + IconSnowflake, + IconSnowflakeOff, + IconSnowman, + IconSoccerField, + IconSocial, + IconSocialOff, + IconSock, + IconSofa, + IconSofaOff, + IconSolarPanel, + IconSolarPanel2, + IconSort09, + IconSort90, + IconSortAZ, + IconSortAscending, + IconSortAscending2, + IconSortAscendingLetters, + IconSortAscendingNumbers, + IconSortDescending, + IconSortDescending2, + IconSortDescendingLetters, + IconSortDescendingNumbers, + IconSortZA, + IconSos, + IconSoup, + IconSoupOff, + IconSourceCode, + IconSpace, + IconSpaceOff, + IconSpacingHorizontal, + IconSpacingVertical, + IconSpade, + IconSparkles, + IconSpeakerphone, + IconSpeedboat, + IconSphere, + IconSphereOff, + IconSpherePlus, + IconSpider, + IconSpiral, + IconSpiralOff, + IconSportBillard, + IconSpray, + IconSpy, + IconSpyOff, + IconSql, + IconSquare, + IconSquareArrowDown, + IconSquareArrowLeft, + IconSquareArrowRight, + IconSquareArrowUp, + IconSquareAsterisk, + IconSquareCheck, + IconSquareChevronDown, + IconSquareChevronLeft, + IconSquareChevronRight, + IconSquareChevronUp, + IconSquareChevronsDown, + IconSquareChevronsLeft, + IconSquareChevronsRight, + IconSquareChevronsUp, + IconSquareDot, + IconSquareF0, + IconSquareF1, + IconSquareF2, + IconSquareF3, + IconSquareF4, + IconSquareF5, + IconSquareF6, + IconSquareF7, + IconSquareF8, + IconSquareF9, + IconSquareForbid, + IconSquareForbid2, + IconSquareHalf, + IconSquareKey, + IconSquareLetterA, + IconSquareLetterB, + IconSquareLetterC, + IconSquareLetterD, + IconSquareLetterE, + IconSquareLetterF, + IconSquareLetterG, + IconSquareLetterH, + IconSquareLetterI, + IconSquareLetterJ, + IconSquareLetterK, + IconSquareLetterL, + IconSquareLetterM, + IconSquareLetterN, + IconSquareLetterO, + IconSquareLetterP, + IconSquareLetterQ, + IconSquareLetterR, + IconSquareLetterS, + IconSquareLetterT, + IconSquareLetterU, + IconSquareLetterV, + IconSquareLetterW, + IconSquareLetterX, + IconSquareLetterY, + IconSquareLetterZ, + IconSquareMinus, + IconSquareNumber0, + IconSquareNumber1, + IconSquareNumber2, + IconSquareNumber3, + IconSquareNumber4, + IconSquareNumber5, + IconSquareNumber6, + IconSquareNumber7, + IconSquareNumber8, + IconSquareNumber9, + IconSquareOff, + IconSquarePlus, + IconSquareRoot, + IconSquareRoot2, + IconSquareRotated, + IconSquareRotatedForbid, + IconSquareRotatedForbid2, + IconSquareRotatedOff, + IconSquareRounded, + IconSquareRoundedArrowDown, + IconSquareRoundedArrowLeft, + IconSquareRoundedArrowRight, + IconSquareRoundedArrowUp, + IconSquareRoundedCheck, + IconSquareRoundedChevronDown, + IconSquareRoundedChevronLeft, + IconSquareRoundedChevronRight, + IconSquareRoundedChevronUp, + IconSquareRoundedChevronsDown, + IconSquareRoundedChevronsLeft, + IconSquareRoundedChevronsRight, + IconSquareRoundedChevronsUp, + IconSquareRoundedLetterA, + IconSquareRoundedLetterB, + IconSquareRoundedLetterC, + IconSquareRoundedLetterD, + IconSquareRoundedLetterE, + IconSquareRoundedLetterF, + IconSquareRoundedLetterG, + IconSquareRoundedLetterH, + IconSquareRoundedLetterI, + IconSquareRoundedLetterJ, + IconSquareRoundedLetterK, + IconSquareRoundedLetterL, + IconSquareRoundedLetterM, + IconSquareRoundedLetterN, + IconSquareRoundedLetterO, + IconSquareRoundedLetterP, + IconSquareRoundedLetterQ, + IconSquareRoundedLetterR, + IconSquareRoundedLetterS, + IconSquareRoundedLetterT, + IconSquareRoundedLetterU, + IconSquareRoundedLetterV, + IconSquareRoundedLetterW, + IconSquareRoundedLetterX, + IconSquareRoundedLetterY, + IconSquareRoundedLetterZ, + IconSquareRoundedMinus, + IconSquareRoundedNumber0, + IconSquareRoundedNumber1, + IconSquareRoundedNumber2, + IconSquareRoundedNumber3, + IconSquareRoundedNumber4, + IconSquareRoundedNumber5, + IconSquareRoundedNumber6, + IconSquareRoundedNumber7, + IconSquareRoundedNumber8, + IconSquareRoundedNumber9, + IconSquareRoundedPlus, + IconSquareRoundedX, + IconSquareToggle, + IconSquareToggleHorizontal, + IconSquareX, + IconSquaresDiagonal, + IconStack, + IconStack2, + IconStack3, + IconStackPop, + IconStackPush, + IconStairs, + IconStairsDown, + IconStairsUp, + IconStar, + IconStarHalf, + IconStarOff, + IconStars, + IconStarsOff, + IconStatusChange, + IconSteam, + IconSteeringWheel, + IconSteeringWheelOff, + IconStepInto, + IconStepOut, + IconStereoGlasses, + IconStethoscope, + IconStethoscopeOff, + IconSticker, + IconStorm, + IconStormOff, + IconStretching, + IconStretching2, + IconStrikethrough, + IconSubmarine, + IconSubscript, + IconSubtask, + IconSum, + IconSumOff, + IconSun, + IconSunHigh, + IconSunLow, + IconSunMoon, + IconSunOff, + IconSunWind, + IconSunglasses, + IconSunrise, + IconSunset, + IconSunset2, + IconSuperscript, + IconSvg, + IconSwimming, + IconSwipe, + IconSwitch, + IconSwitch2, + IconSwitch3, + IconSwitchHorizontal, + IconSwitchVertical, + IconSword, + IconSwordOff, + IconSwords, + IconTable, + IconTableAlias, + IconTableColumn, + IconTableDown, + IconTableExport, + IconTableHeart, + IconTableImport, + IconTableMinus, + IconTableOff, + IconTableOptions, + IconTablePlus, + IconTableRow, + IconTableShare, + IconTableShortcut, + IconTag, + IconTagOff, + IconTags, + IconTagsOff, + IconTallymark1, + IconTallymark2, + IconTallymark3, + IconTallymark4, + IconTallymarks, + IconTank, + IconTarget, + IconTargetArrow, + IconTargetOff, + IconTeapot, + IconTelescope, + IconTelescopeOff, + IconTemperature, + IconTemperatureCelsius, + IconTemperatureFahrenheit, + IconTemperatureMinus, + IconTemperatureOff, + IconTemperaturePlus, + IconTemplate, + IconTemplateOff, + IconTent, + IconTentOff, + IconTerminal, + IconTerminal2, + IconTestPipe, + IconTestPipe2, + IconTestPipeOff, + IconTex, + IconTextCaption, + IconTextColor, + IconTextDecrease, + IconTextDirectionLtr, + IconTextDirectionRtl, + IconTextIncrease, + IconTextOrientation, + IconTextPlus, + IconTextRecognition, + IconTextResize, + IconTextSize, + IconTextSpellcheck, + IconTextWrap, + IconTextWrapDisabled, + IconTexture, + IconTheater, + IconThermometer, + IconThumbDown, + IconThumbDownOff, + IconThumbUp, + IconThumbUpOff, + IconTicTac, + IconTicket, + IconTicketOff, + IconTie, + IconTilde, + IconTiltShift, + IconTiltShiftOff, + IconTimeDuration0, + IconTimeDuration10, + IconTimeDuration15, + IconTimeDuration30, + IconTimeDuration45, + IconTimeDuration5, + IconTimeDuration60, + IconTimeDuration90, + IconTimeDurationOff, + IconTimeline, + IconTimelineEvent, + IconTimelineEventExclamation, + IconTimelineEventMinus, + IconTimelineEventPlus, + IconTimelineEventText, + IconTimelineEventX, + IconTir, + IconToggleLeft, + IconToggleRight, + IconToiletPaper, + IconToiletPaperOff, + IconToml, + IconTool, + IconTools, + IconToolsKitchen, + IconToolsKitchen2, + IconToolsKitchen2Off, + IconToolsKitchenOff, + IconToolsOff, + IconTooltip, + IconTopologyBus, + IconTopologyComplex, + IconTopologyFull, + IconTopologyFullHierarchy, + IconTopologyRing, + IconTopologyRing2, + IconTopologyRing3, + IconTopologyStar, + IconTopologyStar2, + IconTopologyStar3, + IconTopologyStarRing, + IconTopologyStarRing2, + IconTopologyStarRing3, + IconTorii, + IconTornado, + IconTournament, + IconTower, + IconTowerOff, + IconTrack, + IconTractor, + IconTrademark, + IconTrafficCone, + IconTrafficConeOff, + IconTrafficLights, + IconTrafficLightsOff, + IconTrain, + IconTransfer, + IconTransferIn, + IconTransferOut, + IconTransferVertical, + IconTransform, + IconTransitionBottom, + IconTransitionLeft, + IconTransitionRight, + IconTransitionTop, + IconTrash, + IconTrashOff, + IconTrashX, + IconTreadmill, + IconTree, + IconTrees, + IconTrekking, + IconTrendingDown, + IconTrendingDown2, + IconTrendingDown3, + IconTrendingUp, + IconTrendingUp2, + IconTrendingUp3, + IconTriangle, + IconTriangleInverted, + IconTriangleOff, + IconTriangleSquareCircle, + IconTriangles, + IconTrident, + IconTrolley, + IconTrophy, + IconTrophyOff, + IconTrowel, + IconTruck, + IconTruckDelivery, + IconTruckLoading, + IconTruckOff, + IconTruckReturn, + IconTxt, + IconTypography, + IconTypographyOff, + IconUfo, + IconUfoOff, + IconUmbrella, + IconUmbrellaOff, + IconUnderline, + IconUnlink, + IconUpload, + IconUrgent, + IconUsb, + IconUser, + IconUserBolt, + IconUserCancel, + IconUserCheck, + IconUserCircle, + IconUserCode, + IconUserCog, + IconUserDollar, + IconUserDown, + IconUserEdit, + IconUserExclamation, + IconUserHeart, + IconUserMinus, + IconUserOff, + IconUserPause, + IconUserPin, + IconUserPlus, + IconUserQuestion, + IconUserSearch, + IconUserShare, + IconUserShield, + IconUserStar, + IconUserUp, + IconUserX, + IconUsers, + IconUsersGroup, + IconUsersMinus, + IconUsersPlus, + IconUvIndex, + IconUxCircle, + IconVaccine, + IconVaccineBottle, + IconVaccineBottleOff, + IconVaccineOff, + IconVacuumCleaner, + IconVariable, + IconVariableMinus, + IconVariableOff, + IconVariablePlus, + IconVector, + IconVectorBezier, + IconVectorBezier2, + IconVectorBezierArc, + IconVectorBezierCircle, + IconVectorOff, + IconVectorSpline, + IconVectorTriangle, + IconVectorTriangleOff, + IconVenus, + IconVersions, + IconVersionsOff, + IconVideo, + IconVideoMinus, + IconVideoOff, + IconVideoPlus, + IconView360, + IconView360Off, + IconViewfinder, + IconViewfinderOff, + IconViewportNarrow, + IconViewportWide, + IconVinyl, + IconVip, + IconVipOff, + IconVirus, + IconVirusOff, + IconVirusSearch, + IconVocabulary, + IconVocabularyOff, + IconVolcano, + IconVolume, + IconVolume2, + IconVolume3, + IconVolumeOff, + IconWalk, + IconWall, + IconWallOff, + IconWallet, + IconWalletOff, + IconWallpaper, + IconWallpaperOff, + IconWand, + IconWandOff, + IconWash, + IconWashDry, + IconWashDry1, + IconWashDry2, + IconWashDry3, + IconWashDryA, + IconWashDryDip, + IconWashDryF, + IconWashDryFlat, + IconWashDryHang, + IconWashDryOff, + IconWashDryP, + IconWashDryShade, + IconWashDryW, + IconWashDryclean, + IconWashDrycleanOff, + IconWashEco, + IconWashGentle, + IconWashHand, + IconWashMachine, + IconWashOff, + IconWashPress, + IconWashTemperature1, + IconWashTemperature2, + IconWashTemperature3, + IconWashTemperature4, + IconWashTemperature5, + IconWashTemperature6, + IconWashTumbleDry, + IconWashTumbleOff, + IconWaterpolo, + IconWaveSawTool, + IconWaveSine, + IconWaveSquare, + IconWebhook, + IconWebhookOff, + IconWeight, + IconWheelchair, + IconWheelchairOff, + IconWhirl, + IconWifi, + IconWifi0, + IconWifi1, + IconWifi2, + IconWifiOff, + IconWind, + IconWindOff, + IconWindow, + IconWindowMaximize, + IconWindowMinimize, + IconWindowOff, + IconWindsock, + IconWiper, + IconWiperWash, + IconWoman, + IconWood, + IconWorld, + IconWorldBolt, + IconWorldCancel, + IconWorldCheck, + IconWorldCode, + IconWorldCog, + IconWorldDollar, + IconWorldDown, + IconWorldDownload, + IconWorldExclamation, + IconWorldHeart, + IconWorldLatitude, + IconWorldLongitude, + IconWorldMinus, + IconWorldOff, + IconWorldPause, + IconWorldPin, + IconWorldPlus, + IconWorldQuestion, + IconWorldSearch, + IconWorldShare, + IconWorldStar, + IconWorldUp, + IconWorldUpload, + IconWorldWww, + IconWorldX, + IconWreckingBall, + IconWriting, + IconWritingOff, + IconWritingSign, + IconWritingSignOff, + IconX, + IconXboxA, + IconXboxB, + IconXboxX, + IconXboxY, + IconXd, + IconXxx, + IconYinYang, + IconYoga, + IconZeppelin, + IconZeppelinOff, + IconZip, + IconZodiacAquarius, + IconZodiacAries, + IconZodiacCancer, + IconZodiacCapricorn, + IconZodiacGemini, + IconZodiacLeo, + IconZodiacLibra, + IconZodiacPisces, + IconZodiacSagittarius, + IconZodiacScorpio, + IconZodiacTaurus, + IconZodiacVirgo, + IconZoomCancel, + IconZoomCheck, + IconZoomCode, + IconZoomExclamation, + IconZoomIn, + IconZoomInArea, + IconZoomMoney, + IconZoomOut, + IconZoomOutArea, + IconZoomPan, + IconZoomQuestion, + IconZoomReplace, + IconZoomReset, + IconZzz, + IconZzzOff, +} from '@tabler/icons-react'; + +export const ALL_ICONS = { + Icon123, + IconAB, + IconAB2, + IconABOff, + IconAbacus, + IconAbacusOff, + IconAbc, + IconAccessPoint, + IconAccessPointOff, + IconAccessible, + IconAccessibleOff, + IconActivity, + IconActivityHeartbeat, + IconAd, + IconAd2, + IconAdCircle, + IconAdCircleOff, + IconAdOff, + IconAddressBookOff, + IconAdjustments, + IconAdjustmentsAlt, + IconAdjustmentsBolt, + IconAdjustmentsCancel, + IconAdjustmentsCheck, + IconAdjustmentsCode, + IconAdjustmentsCog, + IconAdjustmentsDollar, + IconAdjustmentsDown, + IconAdjustmentsExclamation, + IconAdjustmentsHeart, + IconAdjustmentsHorizontal, + IconAdjustmentsMinus, + IconAdjustmentsOff, + IconAdjustmentsPause, + IconAdjustmentsPin, + IconAdjustmentsPlus, + IconAdjustmentsQuestion, + IconAdjustmentsSearch, + IconAdjustmentsShare, + IconAdjustmentsStar, + IconAdjustmentsUp, + IconAdjustmentsX, + IconAerialLift, + IconAffiliate, + IconAirBalloon, + IconAirConditioning, + IconAirConditioningDisabled, + IconAirTrafficControl, + IconAlarm, + IconAlarmMinus, + IconAlarmOff, + IconAlarmPlus, + IconAlarmSnooze, + IconAlbum, + IconAlbumOff, + IconAlertCircle, + IconAlertHexagon, + IconAlertOctagon, + IconAlertSmall, + IconAlertSquare, + IconAlertSquareRounded, + IconAlertTriangle, + IconAlien, + IconAlignBoxBottomCenter, + IconAlignBoxBottomLeft, + IconAlignBoxBottomRight, + IconAlignBoxCenterBottom, + IconAlignBoxCenterMiddle, + IconAlignBoxCenterStretch, + IconAlignBoxCenterTop, + IconAlignBoxLeftBottom, + IconAlignBoxLeftMiddle, + IconAlignBoxLeftStretch, + IconAlignBoxLeftTop, + IconAlignBoxRightBottom, + IconAlignBoxRightMiddle, + IconAlignBoxRightStretch, + IconAlignBoxRightTop, + IconAlignBoxTopCenter, + IconAlignBoxTopLeft, + IconAlignBoxTopRight, + IconAlignCenter, + IconAlignJustified, + IconAlignLeft, + IconAlignRight, + IconAlpha, + IconAlphabetCyrillic, + IconAlphabetGreek, + IconAlphabetLatin, + IconAmbulance, + IconAmpersand, + IconAnalyze, + IconAnalyzeOff, + IconAnchor, + IconAnchorOff, + IconAngle, + IconAnkh, + IconAntenna, + IconAntennaBars1, + IconAntennaBars2, + IconAntennaBars3, + IconAntennaBars4, + IconAntennaBars5, + IconAntennaBarsOff, + IconAntennaOff, + IconAperture, + IconApertureOff, + IconApi, + IconApiApp, + IconApiAppOff, + IconApiOff, + IconAppWindow, + IconApple, + IconApps, + IconAppsOff, + IconArchive, + IconArchiveOff, + IconArmchair, + IconArmchair2, + IconArmchair2Off, + IconArmchairOff, + IconArrowAutofitContent, + IconArrowAutofitDown, + IconArrowAutofitHeight, + IconArrowAutofitLeft, + IconArrowAutofitRight, + IconArrowAutofitUp, + IconArrowAutofitWidth, + IconArrowBack, + IconArrowBackUp, + IconArrowBackUpDouble, + IconArrowBadgeDown, + IconArrowBadgeLeft, + IconArrowBadgeRight, + IconArrowBadgeUp, + IconArrowBarBoth, + IconArrowBarDown, + IconArrowBarLeft, + IconArrowBarRight, + IconArrowBarToDown, + IconArrowBarToLeft, + IconArrowBarToRight, + IconArrowBarToUp, + IconArrowBarUp, + IconArrowBearLeft, + IconArrowBearLeft2, + IconArrowBearRight, + IconArrowBearRight2, + IconArrowBigDown, + IconArrowBigDownLine, + IconArrowBigDownLines, + IconArrowBigLeft, + IconArrowBigLeftLine, + IconArrowBigLeftLines, + IconArrowBigRight, + IconArrowBigRightLine, + IconArrowBigRightLines, + IconArrowBigUp, + IconArrowBigUpLine, + IconArrowBigUpLines, + IconArrowBounce, + IconArrowCapsule, + IconArrowCurveLeft, + IconArrowCurveRight, + IconArrowDown, + IconArrowDownBar, + IconArrowDownCircle, + IconArrowDownLeft, + IconArrowDownLeftCircle, + IconArrowDownRhombus, + IconArrowDownRight, + IconArrowDownRightCircle, + IconArrowDownSquare, + IconArrowDownTail, + IconArrowElbowLeft, + IconArrowElbowRight, + IconArrowFork, + IconArrowForward, + IconArrowForwardUp, + IconArrowForwardUpDouble, + IconArrowGuide, + IconArrowIteration, + IconArrowLeft, + IconArrowLeftBar, + IconArrowLeftCircle, + IconArrowLeftRhombus, + IconArrowLeftRight, + IconArrowLeftSquare, + IconArrowLeftTail, + IconArrowLoopLeft, + IconArrowLoopLeft2, + IconArrowLoopRight, + IconArrowLoopRight2, + IconArrowMerge, + IconArrowMergeBoth, + IconArrowMergeLeft, + IconArrowMergeRight, + IconArrowMoveDown, + IconArrowMoveLeft, + IconArrowMoveRight, + IconArrowMoveUp, + IconArrowNarrowDown, + IconArrowNarrowLeft, + IconArrowNarrowRight, + IconArrowNarrowUp, + IconArrowRampLeft, + IconArrowRampLeft2, + IconArrowRampLeft3, + IconArrowRampRight, + IconArrowRampRight2, + IconArrowRampRight3, + IconArrowRight, + IconArrowRightBar, + IconArrowRightCircle, + IconArrowRightRhombus, + IconArrowRightSquare, + IconArrowRightTail, + IconArrowRotaryFirstLeft, + IconArrowRotaryFirstRight, + IconArrowRotaryLastLeft, + IconArrowRotaryLastRight, + IconArrowRotaryLeft, + IconArrowRotaryRight, + IconArrowRotaryStraight, + IconArrowRoundaboutLeft, + IconArrowRoundaboutRight, + IconArrowSharpTurnLeft, + IconArrowSharpTurnRight, + IconArrowUp, + IconArrowUpBar, + IconArrowUpCircle, + IconArrowUpLeft, + IconArrowUpLeftCircle, + IconArrowUpRhombus, + IconArrowUpRight, + IconArrowUpRightCircle, + IconArrowUpSquare, + IconArrowUpTail, + IconArrowWaveLeftDown, + IconArrowWaveLeftUp, + IconArrowWaveRightDown, + IconArrowWaveRightUp, + IconArrowZigZag, + IconArrowsCross, + IconArrowsDiagonal, + IconArrowsDiagonal2, + IconArrowsDiagonalMinimize, + IconArrowsDiagonalMinimize2, + IconArrowsDiff, + IconArrowsDoubleNeSw, + IconArrowsDoubleNwSe, + IconArrowsDoubleSeNw, + IconArrowsDoubleSwNe, + IconArrowsDown, + IconArrowsDownUp, + IconArrowsExchange, + IconArrowsExchange2, + IconArrowsHorizontal, + IconArrowsJoin, + IconArrowsJoin2, + IconArrowsLeft, + IconArrowsLeftDown, + IconArrowsLeftRight, + IconArrowsMaximize, + IconArrowsMinimize, + IconArrowsMove, + IconArrowsMoveHorizontal, + IconArrowsMoveVertical, + IconArrowsRandom, + IconArrowsRight, + IconArrowsRightDown, + IconArrowsRightLeft, + IconArrowsShuffle, + IconArrowsShuffle2, + IconArrowsSort, + IconArrowsSplit, + IconArrowsSplit2, + IconArrowsTransferDown, + IconArrowsTransferUp, + IconArrowsUp, + IconArrowsUpDown, + IconArrowsUpLeft, + IconArrowsUpRight, + IconArrowsVertical, + IconArtboard, + IconArtboardOff, + IconArticle, + IconArticleOff, + IconAspectRatio, + IconAspectRatioOff, + IconAssembly, + IconAssemblyOff, + IconAsset, + IconAsterisk, + IconAsteriskSimple, + IconAt, + IconAtOff, + IconAtom, + IconAtom2, + IconAtomOff, + IconAugmentedReality, + IconAugmentedReality2, + IconAugmentedRealityOff, + IconAuth2fa, + IconAward, + IconAwardOff, + IconAxe, + IconAxisX, + IconAxisY, + IconBabyBottle, + IconBabyCarriage, + IconBackhoe, + IconBackpack, + IconBackpackOff, + IconBackslash, + IconBackspace, + IconBadge, + IconBadge3d, + IconBadge4k, + IconBadge8k, + IconBadgeAd, + IconBadgeAr, + IconBadgeCc, + IconBadgeHd, + IconBadgeOff, + IconBadgeSd, + IconBadgeTm, + IconBadgeVo, + IconBadgeVr, + IconBadgeWc, + IconBadges, + IconBadgesOff, + IconBaguette, + IconBallAmericanFootball, + IconBallAmericanFootballOff, + IconBallBaseball, + IconBallBasketball, + IconBallBowling, + IconBallFootball, + IconBallFootballOff, + IconBallTennis, + IconBallVolleyball, + IconBalloon, + IconBalloonOff, + IconBallpen, + IconBallpenOff, + IconBan, + IconBandage, + IconBandageOff, + IconBarbell, + IconBarbellOff, + IconBarcode, + IconBarcodeOff, + IconBarrel, + IconBarrelOff, + IconBarrierBlock, + IconBarrierBlockOff, + IconBaseline, + IconBaselineDensityLarge, + IconBaselineDensityMedium, + IconBaselineDensitySmall, + IconBasket, + IconBasketBolt, + IconBasketCancel, + IconBasketCheck, + IconBasketCode, + IconBasketCog, + IconBasketDiscount, + IconBasketDollar, + IconBasketDown, + IconBasketExclamation, + IconBasketHeart, + IconBasketMinus, + IconBasketOff, + IconBasketPause, + IconBasketPin, + IconBasketPlus, + IconBasketQuestion, + IconBasketSearch, + IconBasketShare, + IconBasketStar, + IconBasketUp, + IconBasketX, + IconBat, + IconBath, + IconBathOff, + IconBattery, + IconBattery1, + IconBattery2, + IconBattery3, + IconBattery4, + IconBatteryAutomotive, + IconBatteryCharging, + IconBatteryCharging2, + IconBatteryEco, + IconBatteryOff, + IconBeach, + IconBeachOff, + IconBed, + IconBedOff, + IconBeer, + IconBeerOff, + IconBell, + IconBellBolt, + IconBellCancel, + IconBellCheck, + IconBellCode, + IconBellCog, + IconBellDollar, + IconBellDown, + IconBellExclamation, + IconBellHeart, + IconBellMinus, + IconBellOff, + IconBellPause, + IconBellPin, + IconBellPlus, + IconBellQuestion, + IconBellRinging, + IconBellRinging2, + IconBellSchool, + IconBellSearch, + IconBellShare, + IconBellStar, + IconBellUp, + IconBellX, + IconBellZ, + IconBeta, + IconBible, + IconBike, + IconBikeOff, + IconBinary, + IconBinaryOff, + IconBinaryTree, + IconBinaryTree2, + IconBiohazard, + IconBiohazardOff, + IconBlade, + IconBleach, + IconBleachChlorine, + IconBleachNoChlorine, + IconBleachOff, + IconBlockquote, + IconBluetooth, + IconBluetoothConnected, + IconBluetoothOff, + IconBluetoothX, + IconBlur, + IconBlurOff, + IconBmp, + IconBold, + IconBoldOff, + IconBolt, + IconBoltOff, + IconBomb, + IconBone, + IconBoneOff, + IconBong, + IconBongOff, + IconBook, + IconBook2, + IconBookDownload, + IconBookOff, + IconBookUpload, + IconBookmark, + IconBookmarkEdit, + IconBookmarkMinus, + IconBookmarkOff, + IconBookmarkPlus, + IconBookmarkQuestion, + IconBookmarks, + IconBookmarksOff, + IconBooks, + IconBooksOff, + IconBorderAll, + IconBorderBottom, + IconBorderCorners, + IconBorderHorizontal, + IconBorderInner, + IconBorderLeft, + IconBorderNone, + IconBorderOuter, + IconBorderRadius, + IconBorderRight, + IconBorderSides, + IconBorderStyle, + IconBorderStyle2, + IconBorderTop, + IconBorderVertical, + IconBottle, + IconBottleOff, + IconBounceLeft, + IconBounceRight, + IconBow, + IconBowl, + IconBox, + IconBoxAlignBottom, + IconBoxAlignBottomLeft, + IconBoxAlignBottomRight, + IconBoxAlignLeft, + IconBoxAlignRight, + IconBoxAlignTop, + IconBoxAlignTopLeft, + IconBoxAlignTopRight, + IconBoxMargin, + IconBoxModel, + IconBoxModel2, + IconBoxModel2Off, + IconBoxModelOff, + IconBoxMultiple, + IconBoxMultiple0, + IconBoxMultiple1, + IconBoxMultiple2, + IconBoxMultiple3, + IconBoxMultiple4, + IconBoxMultiple5, + IconBoxMultiple6, + IconBoxMultiple7, + IconBoxMultiple8, + IconBoxMultiple9, + IconBoxOff, + IconBoxPadding, + IconBraces, + IconBracesOff, + IconBrackets, + IconBracketsContain, + IconBracketsContainEnd, + IconBracketsContainStart, + IconBracketsOff, + IconBraille, + IconBrain, + IconBrand4chan, + IconBrandAbstract, + IconBrandAdobe, + IconBrandAdonisJs, + IconBrandAirbnb, + IconBrandAirtable, + IconBrandAlgolia, + IconBrandAlipay, + IconBrandAlpineJs, + IconBrandAmazon, + IconBrandAmd, + IconBrandAmigo, + IconBrandAmongUs, + IconBrandAndroid, + IconBrandAngular, + IconBrandAnsible, + IconBrandAo3, + IconBrandAppgallery, + IconBrandApple, + IconBrandAppleArcade, + IconBrandApplePodcast, + IconBrandAppstore, + IconBrandAsana, + IconBrandAws, + IconBrandAzure, + IconBrandBackbone, + IconBrandBadoo, + IconBrandBaidu, + IconBrandBandcamp, + IconBrandBandlab, + IconBrandBeats, + IconBrandBehance, + IconBrandBilibili, + IconBrandBinance, + IconBrandBing, + IconBrandBitbucket, + IconBrandBlackberry, + IconBrandBlender, + IconBrandBlogger, + IconBrandBooking, + IconBrandBootstrap, + IconBrandBulma, + IconBrandBumble, + IconBrandBunpo, + IconBrandCSharp, + IconBrandCake, + IconBrandCakephp, + IconBrandCampaignmonitor, + IconBrandCarbon, + IconBrandCashapp, + IconBrandChrome, + IconBrandCinema4d, + IconBrandCitymapper, + IconBrandCloudflare, + IconBrandCodecov, + IconBrandCodepen, + IconBrandCodesandbox, + IconBrandCohost, + IconBrandCoinbase, + IconBrandComedyCentral, + IconBrandCoreos, + IconBrandCouchdb, + IconBrandCouchsurfing, + IconBrandCpp, + IconBrandCraft, + IconBrandCrunchbase, + IconBrandCss3, + IconBrandCtemplar, + IconBrandCucumber, + IconBrandCupra, + IconBrandCypress, + IconBrandD3, + IconBrandDaysCounter, + IconBrandDcos, + IconBrandDebian, + IconBrandDeezer, + IconBrandDeliveroo, + IconBrandDeno, + IconBrandDenodo, + IconBrandDeviantart, + IconBrandDigg, + IconBrandDingtalk, + IconBrandDiscord, + IconBrandDisney, + IconBrandDisqus, + IconBrandDjango, + IconBrandDocker, + IconBrandDoctrine, + IconBrandDolbyDigital, + IconBrandDouban, + IconBrandDribbble, + IconBrandDrops, + IconBrandDrupal, + IconBrandEdge, + IconBrandElastic, + IconBrandElectronicArts, + IconBrandEmber, + IconBrandEnvato, + IconBrandEtsy, + IconBrandEvernote, + IconBrandFacebook, + IconBrandFeedly, + IconBrandFigma, + IconBrandFilezilla, + IconBrandFinder, + IconBrandFirebase, + IconBrandFirefox, + IconBrandFiverr, + IconBrandFlickr, + IconBrandFlightradar24, + IconBrandFlipboard, + IconBrandFlutter, + IconBrandFortnite, + IconBrandFoursquare, + IconBrandFramer, + IconBrandFramerMotion, + IconBrandFunimation, + IconBrandGatsby, + IconBrandGit, + IconBrandGithub, + IconBrandGithubCopilot, + IconBrandGitlab, + IconBrandGmail, + IconBrandGolang, + IconBrandGoogle, + IconBrandGoogleAnalytics, + IconBrandGoogleBigQuery, + IconBrandGoogleDrive, + IconBrandGoogleFit, + IconBrandGoogleHome, + IconBrandGoogleMaps, + IconBrandGoogleOne, + IconBrandGooglePhotos, + IconBrandGooglePlay, + IconBrandGooglePodcasts, + IconBrandGrammarly, + IconBrandGraphql, + IconBrandGravatar, + IconBrandGrindr, + IconBrandGuardian, + IconBrandGumroad, + IconBrandHbo, + IconBrandHeadlessui, + IconBrandHexo, + IconBrandHipchat, + IconBrandHtml5, + IconBrandInertia, + IconBrandInstagram, + IconBrandIntercom, + IconBrandItch, + IconBrandJavascript, + IconBrandJuejin, + IconBrandKbin, + IconBrandKick, + IconBrandKickstarter, + IconBrandKotlin, + IconBrandLaravel, + IconBrandLastfm, + IconBrandLeetcode, + IconBrandLetterboxd, + IconBrandLine, + IconBrandLinkedin, + IconBrandLinktree, + IconBrandLinqpad, + IconBrandLoom, + IconBrandMailgun, + IconBrandMantine, + IconBrandMastercard, + IconBrandMastodon, + IconBrandMatrix, + IconBrandMcdonalds, + IconBrandMedium, + IconBrandMercedes, + IconBrandMessenger, + IconBrandMeta, + IconBrandMinecraft, + IconBrandMiniprogram, + IconBrandMixpanel, + IconBrandMonday, + IconBrandMongodb, + IconBrandMyOppo, + IconBrandMysql, + IconBrandNationalGeographic, + IconBrandNem, + IconBrandNetbeans, + IconBrandNeteaseMusic, + IconBrandNetflix, + IconBrandNexo, + IconBrandNextcloud, + IconBrandNextjs, + IconBrandNodejs, + IconBrandNordVpn, + IconBrandNotion, + IconBrandNpm, + IconBrandNuxt, + IconBrandNytimes, + IconBrandOauth, + IconBrandOffice, + IconBrandOkRu, + IconBrandOnedrive, + IconBrandOnlyfans, + IconBrandOpenSource, + IconBrandOpenai, + IconBrandOpenvpn, + IconBrandOpera, + IconBrandPagekit, + IconBrandPatreon, + IconBrandPaypal, + IconBrandPaypay, + IconBrandPeanut, + IconBrandPepsi, + IconBrandPhp, + IconBrandPicsart, + IconBrandPinterest, + IconBrandPlanetscale, + IconBrandPocket, + IconBrandPolymer, + IconBrandPowershell, + IconBrandProducthunt, + IconBrandPushbullet, + IconBrandPushover, + IconBrandPython, + IconBrandQq, + IconBrandRadixUi, + IconBrandReact, + IconBrandReactNative, + IconBrandReason, + IconBrandReddit, + IconBrandRedhat, + IconBrandRedux, + IconBrandRevolut, + IconBrandRumble, + IconBrandRust, + IconBrandSafari, + IconBrandSamsungpass, + IconBrandSass, + IconBrandSentry, + IconBrandSharik, + IconBrandShazam, + IconBrandShopee, + IconBrandSketch, + IconBrandSkype, + IconBrandSlack, + IconBrandSnapchat, + IconBrandSnapseed, + IconBrandSnowflake, + IconBrandSocketIo, + IconBrandSolidjs, + IconBrandSoundcloud, + IconBrandSpacehey, + IconBrandSpeedtest, + IconBrandSpotify, + IconBrandStackoverflow, + IconBrandStackshare, + IconBrandSteam, + IconBrandStorj, + IconBrandStorybook, + IconBrandStorytel, + IconBrandStrava, + IconBrandStripe, + IconBrandSublimeText, + IconBrandSugarizer, + IconBrandSupabase, + IconBrandSuperhuman, + IconBrandSupernova, + IconBrandSurfshark, + IconBrandSvelte, + IconBrandSwift, + IconBrandSymfony, + IconBrandTabler, + IconBrandTailwind, + IconBrandTaobao, + IconBrandTed, + IconBrandTelegram, + IconBrandTerraform, + IconBrandTether, + IconBrandThreads, + IconBrandThreejs, + IconBrandTidal, + IconBrandTiktok, + IconBrandTinder, + IconBrandTopbuzz, + IconBrandTorchain, + IconBrandToyota, + IconBrandTrello, + IconBrandTripadvisor, + IconBrandTumblr, + IconBrandTwilio, + IconBrandTwitch, + IconBrandTwitter, + IconBrandTypescript, + IconBrandUber, + IconBrandUbuntu, + IconBrandUnity, + IconBrandUnsplash, + IconBrandUpwork, + IconBrandValorant, + IconBrandVercel, + IconBrandVimeo, + IconBrandVinted, + IconBrandVisa, + IconBrandVisualStudio, + IconBrandVite, + IconBrandVivaldi, + IconBrandVk, + IconBrandVlc, + IconBrandVolkswagen, + IconBrandVsco, + IconBrandVscode, + IconBrandVue, + IconBrandWalmart, + IconBrandWaze, + IconBrandWebflow, + IconBrandWechat, + IconBrandWeibo, + IconBrandWhatsapp, + IconBrandWikipedia, + IconBrandWindows, + IconBrandWindy, + IconBrandWish, + IconBrandWix, + IconBrandWordpress, + IconBrandX, + IconBrandXamarin, + IconBrandXbox, + IconBrandXdeep, + IconBrandXing, + IconBrandYahoo, + IconBrandYandex, + IconBrandYatse, + IconBrandYcombinator, + IconBrandYoutube, + IconBrandYoutubeKids, + IconBrandZalando, + IconBrandZapier, + IconBrandZeit, + IconBrandZhihu, + IconBrandZoom, + IconBrandZulip, + IconBrandZwift, + IconBread, + IconBreadOff, + IconBriefcase, + IconBriefcase2, + IconBriefcaseOff, + IconBrightness, + IconBrightness2, + IconBrightnessDown, + IconBrightnessHalf, + IconBrightnessOff, + IconBrightnessUp, + IconBroadcast, + IconBroadcastOff, + IconBrowser, + IconBrowserCheck, + IconBrowserMaximize, + IconBrowserOff, + IconBrowserPlus, + IconBrowserX, + IconBrush, + IconBrushOff, + IconBucket, + IconBucketDroplet, + IconBucketOff, + IconBug, + IconBugOff, + IconBuilding, + IconBuildingArch, + IconBuildingBank, + IconBuildingBridge, + IconBuildingBridge2, + IconBuildingBroadcastTower, + IconBuildingCarousel, + IconBuildingCastle, + IconBuildingChurch, + IconBuildingCircus, + IconBuildingCommunity, + IconBuildingCottage, + IconBuildingEstate, + IconBuildingFactory, + IconBuildingFactory2, + IconBuildingFortress, + IconBuildingHospital, + IconBuildingLighthouse, + IconBuildingMonument, + IconBuildingMosque, + IconBuildingPavilion, + IconBuildingSkyscraper, + IconBuildingStadium, + IconBuildingStore, + IconBuildingTunnel, + IconBuildingWarehouse, + IconBuildingWindTurbine, + IconBulb, + IconBulbOff, + IconBulldozer, + IconBus, + IconBusOff, + IconBusStop, + IconBusinessplan, + IconButterfly, + IconCactus, + IconCactusOff, + IconCake, + IconCakeOff, + IconCalculator, + IconCalculatorOff, + IconCalendar, + IconCalendarBolt, + IconCalendarCancel, + IconCalendarCheck, + IconCalendarClock, + IconCalendarCode, + IconCalendarCog, + IconCalendarDollar, + IconCalendarDown, + IconCalendarDue, + IconCalendarEvent, + IconCalendarExclamation, + IconCalendarHeart, + IconCalendarMinus, + IconCalendarOff, + IconCalendarPause, + IconCalendarPin, + IconCalendarPlus, + IconCalendarQuestion, + IconCalendarRepeat, + IconCalendarSearch, + IconCalendarShare, + IconCalendarStar, + IconCalendarStats, + IconCalendarTime, + IconCalendarUp, + IconCalendarX, + IconCamera, + IconCameraBolt, + IconCameraCancel, + IconCameraCheck, + IconCameraCode, + IconCameraCog, + IconCameraDollar, + IconCameraDown, + IconCameraExclamation, + IconCameraHeart, + IconCameraMinus, + IconCameraOff, + IconCameraPause, + IconCameraPin, + IconCameraPlus, + IconCameraQuestion, + IconCameraRotate, + IconCameraSearch, + IconCameraSelfie, + IconCameraShare, + IconCameraStar, + IconCameraUp, + IconCameraX, + IconCamper, + IconCampfire, + IconCandle, + IconCandy, + IconCandyOff, + IconCane, + IconCannabis, + IconCapsule, + IconCapsuleHorizontal, + IconCapture, + IconCaptureOff, + IconCar, + IconCarCrane, + IconCarCrash, + IconCarOff, + IconCarTurbine, + IconCaravan, + IconCardboards, + IconCardboardsOff, + IconCards, + IconCaretDown, + IconCaretLeft, + IconCaretRight, + IconCaretUp, + IconCarouselHorizontal, + IconCarouselVertical, + IconCarrot, + IconCarrotOff, + IconCash, + IconCashBanknote, + IconCashBanknoteOff, + IconCashOff, + IconCast, + IconCastOff, + IconCat, + IconCategory, + IconCategory2, + IconCe, + IconCeOff, + IconCell, + IconCellSignal1, + IconCellSignal2, + IconCellSignal3, + IconCellSignal4, + IconCellSignal5, + IconCellSignalOff, + IconCertificate, + IconCertificate2, + IconCertificate2Off, + IconCertificateOff, + IconChairDirector, + IconChalkboard, + IconChalkboardOff, + IconChargingPile, + IconChartArcs, + IconChartArcs3, + IconChartArea, + IconChartAreaLine, + IconChartArrows, + IconChartArrowsVertical, + IconChartBar, + IconChartBarOff, + IconChartBubble, + IconChartCandle, + IconChartCircles, + IconChartDonut, + IconChartDonut2, + IconChartDonut3, + IconChartDonut4, + IconChartDots, + IconChartDots2, + IconChartDots3, + IconChartGridDots, + IconChartHistogram, + IconChartInfographic, + IconChartLine, + IconChartPie, + IconChartPie2, + IconChartPie3, + IconChartPie4, + IconChartPieOff, + IconChartPpf, + IconChartRadar, + IconChartSankey, + IconChartTreemap, + IconCheck, + IconCheckbox, + IconChecklist, + IconChecks, + IconCheckupList, + IconCheese, + IconChefHat, + IconChefHatOff, + IconCherry, + IconChess, + IconChessBishop, + IconChessKing, + IconChessKnight, + IconChessQueen, + IconChessRook, + IconChevronCompactDown, + IconChevronCompactLeft, + IconChevronCompactRight, + IconChevronCompactUp, + IconChevronDown, + IconChevronDownLeft, + IconChevronDownRight, + IconChevronLeft, + IconChevronLeftPipe, + IconChevronRight, + IconChevronRightPipe, + IconChevronUp, + IconChevronUpLeft, + IconChevronUpRight, + IconChevronsDown, + IconChevronsDownLeft, + IconChevronsDownRight, + IconChevronsLeft, + IconChevronsRight, + IconChevronsUp, + IconChevronsUpLeft, + IconChevronsUpRight, + IconChisel, + IconChristmasTree, + IconChristmasTreeOff, + IconCircle, + IconCircleArrowDown, + IconCircleArrowDownLeft, + IconCircleArrowDownRight, + IconCircleArrowLeft, + IconCircleArrowRight, + IconCircleArrowUp, + IconCircleArrowUpLeft, + IconCircleArrowUpRight, + IconCircleCaretDown, + IconCircleCaretLeft, + IconCircleCaretRight, + IconCircleCaretUp, + IconCircleCheck, + IconCircleChevronDown, + IconCircleChevronLeft, + IconCircleChevronRight, + IconCircleChevronUp, + IconCircleChevronsDown, + IconCircleChevronsLeft, + IconCircleChevronsRight, + IconCircleChevronsUp, + IconCircleDashed, + IconCircleDot, + IconCircleDotted, + IconCircleHalf, + IconCircleHalf2, + IconCircleHalfVertical, + IconCircleKey, + IconCircleLetterA, + IconCircleLetterB, + IconCircleLetterC, + IconCircleLetterD, + IconCircleLetterE, + IconCircleLetterF, + IconCircleLetterG, + IconCircleLetterH, + IconCircleLetterI, + IconCircleLetterJ, + IconCircleLetterK, + IconCircleLetterL, + IconCircleLetterM, + IconCircleLetterN, + IconCircleLetterO, + IconCircleLetterP, + IconCircleLetterQ, + IconCircleLetterR, + IconCircleLetterS, + IconCircleLetterT, + IconCircleLetterU, + IconCircleLetterV, + IconCircleLetterW, + IconCircleLetterX, + IconCircleLetterY, + IconCircleLetterZ, + IconCircleMinus, + IconCircleNumber0, + IconCircleNumber1, + IconCircleNumber2, + IconCircleNumber3, + IconCircleNumber4, + IconCircleNumber5, + IconCircleNumber6, + IconCircleNumber7, + IconCircleNumber8, + IconCircleNumber9, + IconCircleOff, + IconCirclePlus, + IconCircleRectangle, + IconCircleRectangleOff, + IconCircleSquare, + IconCircleTriangle, + IconCircleX, + IconCircles, + IconCirclesRelation, + IconCircuitAmmeter, + IconCircuitBattery, + IconCircuitBulb, + IconCircuitCapacitor, + IconCircuitCapacitorPolarized, + IconCircuitCell, + IconCircuitCellPlus, + IconCircuitChangeover, + IconCircuitDiode, + IconCircuitDiodeZener, + IconCircuitGround, + IconCircuitGroundDigital, + IconCircuitInductor, + IconCircuitMotor, + IconCircuitPushbutton, + IconCircuitResistor, + IconCircuitSwitchClosed, + IconCircuitSwitchOpen, + IconCircuitVoltmeter, + IconClearAll, + IconClearFormatting, + IconClick, + IconClipboard, + IconClipboardCheck, + IconClipboardCopy, + IconClipboardData, + IconClipboardHeart, + IconClipboardList, + IconClipboardOff, + IconClipboardPlus, + IconClipboardText, + IconClipboardTypography, + IconClipboardX, + IconClock, + IconClock2, + IconClockBolt, + IconClockCancel, + IconClockCheck, + IconClockCode, + IconClockCog, + IconClockDollar, + IconClockDown, + IconClockEdit, + IconClockExclamation, + IconClockHeart, + IconClockHour1, + IconClockHour10, + IconClockHour11, + IconClockHour12, + IconClockHour2, + IconClockHour3, + IconClockHour4, + IconClockHour5, + IconClockHour6, + IconClockHour7, + IconClockHour8, + IconClockHour9, + IconClockMinus, + IconClockOff, + IconClockPause, + IconClockPin, + IconClockPlay, + IconClockPlus, + IconClockQuestion, + IconClockRecord, + IconClockSearch, + IconClockShare, + IconClockShield, + IconClockStar, + IconClockStop, + IconClockUp, + IconClockX, + IconClothesRack, + IconClothesRackOff, + IconCloud, + IconCloudBolt, + IconCloudCancel, + IconCloudCheck, + IconCloudCode, + IconCloudCog, + IconCloudComputing, + IconCloudDataConnection, + IconCloudDollar, + IconCloudDown, + IconCloudDownload, + IconCloudExclamation, + IconCloudFog, + IconCloudHeart, + IconCloudLock, + IconCloudLockOpen, + IconCloudMinus, + IconCloudOff, + IconCloudPause, + IconCloudPin, + IconCloudPlus, + IconCloudQuestion, + IconCloudRain, + IconCloudSearch, + IconCloudShare, + IconCloudSnow, + IconCloudStar, + IconCloudStorm, + IconCloudUp, + IconCloudUpload, + IconCloudX, + IconClover, + IconClover2, + IconClubs, + IconCode, + IconCodeCircle, + IconCodeCircle2, + IconCodeDots, + IconCodeMinus, + IconCodeOff, + IconCodePlus, + IconCoffee, + IconCoffeeOff, + IconCoffin, + IconCoin, + IconCoinBitcoin, + IconCoinEuro, + IconCoinMonero, + IconCoinOff, + IconCoinPound, + IconCoinRupee, + IconCoinYen, + IconCoinYuan, + IconCoins, + IconColorFilter, + IconColorPicker, + IconColorPickerOff, + IconColorSwatch, + IconColorSwatchOff, + IconColumnInsertLeft, + IconColumnInsertRight, + IconColumnRemove, + IconColumns, + IconColumns1, + IconColumns2, + IconColumns3, + IconColumnsOff, + IconComet, + IconCommand, + IconCommandOff, + IconCompass, + IconCompassOff, + IconComponents, + IconComponentsOff, + IconCone, + IconCone2, + IconConeOff, + IconConePlus, + IconConfetti, + IconConfettiOff, + IconConfucius, + IconContainer, + IconContainerOff, + IconContrast, + IconContrast2, + IconContrast2Off, + IconContrastOff, + IconCooker, + IconCookie, + IconCookieMan, + IconCookieOff, + IconCopy, + IconCopyOff, + IconCopyleft, + IconCopyleftOff, + IconCopyright, + IconCopyrightOff, + IconCornerDownLeft, + IconCornerDownLeftDouble, + IconCornerDownRight, + IconCornerDownRightDouble, + IconCornerLeftDown, + IconCornerLeftDownDouble, + IconCornerLeftUp, + IconCornerLeftUpDouble, + IconCornerRightDown, + IconCornerRightDownDouble, + IconCornerRightUp, + IconCornerRightUpDouble, + IconCornerUpLeft, + IconCornerUpLeftDouble, + IconCornerUpRight, + IconCornerUpRightDouble, + IconCpu, + IconCpu2, + IconCpuOff, + IconCrane, + IconCraneOff, + IconCreativeCommons, + IconCreativeCommonsBy, + IconCreativeCommonsNc, + IconCreativeCommonsNd, + IconCreativeCommonsOff, + IconCreativeCommonsSa, + IconCreativeCommonsZero, + IconCreditCard, + IconCreditCardOff, + IconCricket, + IconCrop, + IconCross, + IconCrossOff, + IconCrosshair, + IconCrown, + IconCrownOff, + IconCrutches, + IconCrutchesOff, + IconCrystalBall, + IconCsv, + IconCube, + IconCube3dSphere, + IconCube3dSphereOff, + IconCubeOff, + IconCubePlus, + IconCubeSend, + IconCubeUnfolded, + IconCup, + IconCupOff, + IconCurling, + IconCurlyLoop, + IconCurrency, + IconCurrencyAfghani, + IconCurrencyBahraini, + IconCurrencyBaht, + IconCurrencyBitcoin, + IconCurrencyCent, + IconCurrencyDinar, + IconCurrencyDirham, + IconCurrencyDogecoin, + IconCurrencyDollar, + IconCurrencyDollarAustralian, + IconCurrencyDollarBrunei, + IconCurrencyDollarCanadian, + IconCurrencyDollarGuyanese, + IconCurrencyDollarOff, + IconCurrencyDollarSingapore, + IconCurrencyDollarZimbabwean, + IconCurrencyDong, + IconCurrencyDram, + IconCurrencyEthereum, + IconCurrencyEuro, + IconCurrencyEuroOff, + IconCurrencyFlorin, + IconCurrencyForint, + IconCurrencyFrank, + IconCurrencyGuarani, + IconCurrencyHryvnia, + IconCurrencyIranianRial, + IconCurrencyKip, + IconCurrencyKroneCzech, + IconCurrencyKroneDanish, + IconCurrencyKroneSwedish, + IconCurrencyLari, + IconCurrencyLeu, + IconCurrencyLira, + IconCurrencyLitecoin, + IconCurrencyLyd, + IconCurrencyManat, + IconCurrencyMonero, + IconCurrencyNaira, + IconCurrencyNano, + IconCurrencyOff, + IconCurrencyPaanga, + IconCurrencyPeso, + IconCurrencyPound, + IconCurrencyPoundOff, + IconCurrencyQuetzal, + IconCurrencyReal, + IconCurrencyRenminbi, + IconCurrencyRipple, + IconCurrencyRiyal, + IconCurrencyRubel, + IconCurrencyRufiyaa, + IconCurrencyRupee, + IconCurrencyRupeeNepalese, + IconCurrencyShekel, + IconCurrencySolana, + IconCurrencySom, + IconCurrencyTaka, + IconCurrencyTenge, + IconCurrencyTugrik, + IconCurrencyWon, + IconCurrencyYen, + IconCurrencyYenOff, + IconCurrencyYuan, + IconCurrencyZloty, + IconCurrentLocation, + IconCurrentLocationOff, + IconCursorOff, + IconCursorText, + IconCut, + IconCylinder, + IconCylinderOff, + IconCylinderPlus, + IconDashboard, + IconDashboardOff, + IconDatabase, + IconDatabaseCog, + IconDatabaseDollar, + IconDatabaseEdit, + IconDatabaseExclamation, + IconDatabaseExport, + IconDatabaseHeart, + IconDatabaseImport, + IconDatabaseLeak, + IconDatabaseMinus, + IconDatabaseOff, + IconDatabasePlus, + IconDatabaseSearch, + IconDatabaseShare, + IconDatabaseStar, + IconDatabaseX, + IconDecimal, + IconDeer, + IconDelta, + IconDental, + IconDentalBroken, + IconDentalOff, + IconDeselect, + IconDetails, + IconDetailsOff, + IconDeviceAirpods, + IconDeviceAirpodsCase, + IconDeviceAirtag, + IconDeviceAnalytics, + IconDeviceAudioTape, + IconDeviceCameraPhone, + IconDeviceCctv, + IconDeviceCctvOff, + IconDeviceComputerCamera, + IconDeviceComputerCameraOff, + IconDeviceDesktop, + IconDeviceDesktopAnalytics, + IconDeviceDesktopBolt, + IconDeviceDesktopCancel, + IconDeviceDesktopCheck, + IconDeviceDesktopCode, + IconDeviceDesktopCog, + IconDeviceDesktopDollar, + IconDeviceDesktopDown, + IconDeviceDesktopExclamation, + IconDeviceDesktopHeart, + IconDeviceDesktopMinus, + IconDeviceDesktopOff, + IconDeviceDesktopPause, + IconDeviceDesktopPin, + IconDeviceDesktopPlus, + IconDeviceDesktopQuestion, + IconDeviceDesktopSearch, + IconDeviceDesktopShare, + IconDeviceDesktopStar, + IconDeviceDesktopUp, + IconDeviceDesktopX, + IconDeviceFloppy, + IconDeviceGamepad, + IconDeviceGamepad2, + IconDeviceHeartMonitor, + IconDeviceImac, + IconDeviceImacBolt, + IconDeviceImacCancel, + IconDeviceImacCheck, + IconDeviceImacCode, + IconDeviceImacCog, + IconDeviceImacDollar, + IconDeviceImacDown, + IconDeviceImacExclamation, + IconDeviceImacHeart, + IconDeviceImacMinus, + IconDeviceImacOff, + IconDeviceImacPause, + IconDeviceImacPin, + IconDeviceImacPlus, + IconDeviceImacQuestion, + IconDeviceImacSearch, + IconDeviceImacShare, + IconDeviceImacStar, + IconDeviceImacUp, + IconDeviceImacX, + IconDeviceIpad, + IconDeviceIpadBolt, + IconDeviceIpadCancel, + IconDeviceIpadCheck, + IconDeviceIpadCode, + IconDeviceIpadCog, + IconDeviceIpadDollar, + IconDeviceIpadDown, + IconDeviceIpadExclamation, + IconDeviceIpadHeart, + IconDeviceIpadHorizontal, + IconDeviceIpadHorizontalBolt, + IconDeviceIpadHorizontalCancel, + IconDeviceIpadHorizontalCheck, + IconDeviceIpadHorizontalCode, + IconDeviceIpadHorizontalCog, + IconDeviceIpadHorizontalDollar, + IconDeviceIpadHorizontalDown, + IconDeviceIpadHorizontalExclamation, + IconDeviceIpadHorizontalHeart, + IconDeviceIpadHorizontalMinus, + IconDeviceIpadHorizontalOff, + IconDeviceIpadHorizontalPause, + IconDeviceIpadHorizontalPin, + IconDeviceIpadHorizontalPlus, + IconDeviceIpadHorizontalQuestion, + IconDeviceIpadHorizontalSearch, + IconDeviceIpadHorizontalShare, + IconDeviceIpadHorizontalStar, + IconDeviceIpadHorizontalUp, + IconDeviceIpadHorizontalX, + IconDeviceIpadMinus, + IconDeviceIpadOff, + IconDeviceIpadPause, + IconDeviceIpadPin, + IconDeviceIpadPlus, + IconDeviceIpadQuestion, + IconDeviceIpadSearch, + IconDeviceIpadShare, + IconDeviceIpadStar, + IconDeviceIpadUp, + IconDeviceIpadX, + IconDeviceLandlinePhone, + IconDeviceLaptop, + IconDeviceLaptopOff, + IconDeviceMobile, + IconDeviceMobileBolt, + IconDeviceMobileCancel, + IconDeviceMobileCharging, + IconDeviceMobileCheck, + IconDeviceMobileCode, + IconDeviceMobileCog, + IconDeviceMobileDollar, + IconDeviceMobileDown, + IconDeviceMobileExclamation, + IconDeviceMobileHeart, + IconDeviceMobileMessage, + IconDeviceMobileMinus, + IconDeviceMobileOff, + IconDeviceMobilePause, + IconDeviceMobilePin, + IconDeviceMobilePlus, + IconDeviceMobileQuestion, + IconDeviceMobileRotated, + IconDeviceMobileSearch, + IconDeviceMobileShare, + IconDeviceMobileStar, + IconDeviceMobileUp, + IconDeviceMobileVibration, + IconDeviceMobileX, + IconDeviceNintendo, + IconDeviceNintendoOff, + IconDeviceProjector, + IconDeviceRemote, + IconDeviceSdCard, + IconDeviceSim, + IconDeviceSim1, + IconDeviceSim2, + IconDeviceSim3, + IconDeviceSpeaker, + IconDeviceSpeakerOff, + IconDeviceTablet, + IconDeviceTabletBolt, + IconDeviceTabletCancel, + IconDeviceTabletCheck, + IconDeviceTabletCode, + IconDeviceTabletCog, + IconDeviceTabletDollar, + IconDeviceTabletDown, + IconDeviceTabletExclamation, + IconDeviceTabletHeart, + IconDeviceTabletMinus, + IconDeviceTabletOff, + IconDeviceTabletPause, + IconDeviceTabletPin, + IconDeviceTabletPlus, + IconDeviceTabletQuestion, + IconDeviceTabletSearch, + IconDeviceTabletShare, + IconDeviceTabletStar, + IconDeviceTabletUp, + IconDeviceTabletX, + IconDeviceTv, + IconDeviceTvOff, + IconDeviceTvOld, + IconDeviceVisionPro, + IconDeviceWatch, + IconDeviceWatchBolt, + IconDeviceWatchCancel, + IconDeviceWatchCheck, + IconDeviceWatchCode, + IconDeviceWatchCog, + IconDeviceWatchDollar, + IconDeviceWatchDown, + IconDeviceWatchExclamation, + IconDeviceWatchHeart, + IconDeviceWatchMinus, + IconDeviceWatchOff, + IconDeviceWatchPause, + IconDeviceWatchPin, + IconDeviceWatchPlus, + IconDeviceWatchQuestion, + IconDeviceWatchSearch, + IconDeviceWatchShare, + IconDeviceWatchStar, + IconDeviceWatchStats, + IconDeviceWatchStats2, + IconDeviceWatchUp, + IconDeviceWatchX, + IconDevices, + IconDevices2, + IconDevicesBolt, + IconDevicesCancel, + IconDevicesCheck, + IconDevicesCode, + IconDevicesCog, + IconDevicesDollar, + IconDevicesDown, + IconDevicesExclamation, + IconDevicesHeart, + IconDevicesMinus, + IconDevicesOff, + IconDevicesPause, + IconDevicesPc, + IconDevicesPcOff, + IconDevicesPin, + IconDevicesPlus, + IconDevicesQuestion, + IconDevicesSearch, + IconDevicesShare, + IconDevicesStar, + IconDevicesUp, + IconDevicesX, + IconDiabolo, + IconDiaboloOff, + IconDiaboloPlus, + IconDialpad, + IconDialpadOff, + IconDiamond, + IconDiamondOff, + IconDiamonds, + IconDice, + IconDice1, + IconDice2, + IconDice3, + IconDice4, + IconDice5, + IconDice6, + IconDimensions, + IconDirection, + IconDirectionHorizontal, + IconDirectionSign, + IconDirectionSignOff, + IconDirections, + IconDirectionsOff, + IconDisabled, + IconDisabled2, + IconDisabledOff, + IconDisc, + IconDiscGolf, + IconDiscOff, + IconDiscount, + IconDiscountOff, + IconDivide, + IconDna, + IconDna2, + IconDna2Off, + IconDnaOff, + IconDog, + IconDogBowl, + IconDoor, + IconDoorEnter, + IconDoorExit, + IconDoorOff, + IconDots, + IconDotsCircleHorizontal, + IconDotsDiagonal, + IconDotsDiagonal2, + IconDotsVertical, + IconDownload, + IconDownloadOff, + IconDragDrop, + IconDragDrop2, + IconDrone, + IconDroneOff, + IconDropCircle, + IconDroplet, + IconDropletBolt, + IconDropletCancel, + IconDropletCheck, + IconDropletCode, + IconDropletCog, + IconDropletDollar, + IconDropletDown, + IconDropletExclamation, + IconDropletHalf, + IconDropletHalf2, + IconDropletHeart, + IconDropletMinus, + IconDropletOff, + IconDropletPause, + IconDropletPin, + IconDropletPlus, + IconDropletQuestion, + IconDropletSearch, + IconDropletShare, + IconDropletStar, + IconDropletUp, + IconDropletX, + IconDroplets, + IconDualScreen, + IconEPassport, + IconEar, + IconEarOff, + IconEaseIn, + IconEaseInControlPoint, + IconEaseInOut, + IconEaseInOutControlPoints, + IconEaseOut, + IconEaseOutControlPoint, + IconEdit, + IconEditCircle, + IconEditCircleOff, + IconEditOff, + IconEgg, + IconEggCracked, + IconEggFried, + IconEggOff, + IconEggs, + IconElevator, + IconElevatorOff, + IconEmergencyBed, + IconEmpathize, + IconEmpathizeOff, + IconEmphasis, + IconEngine, + IconEngineOff, + IconEqual, + IconEqualDouble, + IconEqualNot, + IconEraser, + IconEraserOff, + IconError404, + IconError404Off, + IconEscalator, + IconEscalatorDown, + IconEscalatorUp, + IconExchange, + IconExchangeOff, + IconExclamationCircle, + IconExclamationMark, + IconExclamationMarkOff, + IconExplicit, + IconExplicitOff, + IconExposure, + IconExposure0, + IconExposureMinus1, + IconExposureMinus2, + IconExposureOff, + IconExposurePlus1, + IconExposurePlus2, + IconExternalLink, + IconExternalLinkOff, + IconEye, + IconEyeBolt, + IconEyeCancel, + IconEyeCheck, + IconEyeClosed, + IconEyeCode, + IconEyeCog, + IconEyeDiscount, + IconEyeDollar, + IconEyeDown, + IconEyeEdit, + IconEyeExclamation, + IconEyeHeart, + IconEyeMinus, + IconEyeOff, + IconEyePause, + IconEyePin, + IconEyePlus, + IconEyeQuestion, + IconEyeSearch, + IconEyeShare, + IconEyeStar, + IconEyeTable, + IconEyeUp, + IconEyeX, + IconEyeglass, + IconEyeglass2, + IconEyeglassOff, + IconFaceId, + IconFaceIdError, + IconFaceMask, + IconFaceMaskOff, + IconFall, + IconFeather, + IconFeatherOff, + IconFence, + IconFenceOff, + IconFidgetSpinner, + IconFile, + IconFile3d, + IconFileAlert, + IconFileAnalytics, + IconFileArrowLeft, + IconFileArrowRight, + IconFileBarcode, + IconFileBroken, + IconFileCertificate, + IconFileChart, + IconFileCheck, + IconFileCode, + IconFileCode2, + IconFileCv, + IconFileDatabase, + IconFileDelta, + IconFileDescription, + IconFileDiff, + IconFileDigit, + IconFileDislike, + IconFileDollar, + IconFileDots, + IconFileDownload, + IconFileEuro, + IconFileExport, + IconFileFunction, + IconFileHorizontal, + IconFileImport, + IconFileInfinity, + IconFileInfo, + IconFileInvoice, + IconFileLambda, + IconFileLike, + IconFileMinus, + IconFileMusic, + IconFileOff, + IconFileOrientation, + IconFilePencil, + IconFilePercent, + IconFilePhone, + IconFilePlus, + IconFilePower, + IconFileReport, + IconFileRss, + IconFileScissors, + IconFileSearch, + IconFileSettings, + IconFileShredder, + IconFileSignal, + IconFileSpreadsheet, + IconFileStack, + IconFileStar, + IconFileSymlink, + IconFileText, + IconFileTextAi, + IconFileTime, + IconFileTypeBmp, + IconFileTypeCss, + IconFileTypeCsv, + IconFileTypeDoc, + IconFileTypeDocx, + IconFileTypeHtml, + IconFileTypeJpg, + IconFileTypeJs, + IconFileTypeJsx, + IconFileTypePdf, + IconFileTypePhp, + IconFileTypePng, + IconFileTypePpt, + IconFileTypeRs, + IconFileTypeSql, + IconFileTypeSvg, + IconFileTypeTs, + IconFileTypeTsx, + IconFileTypeTxt, + IconFileTypeVue, + IconFileTypeXls, + IconFileTypeXml, + IconFileTypeZip, + IconFileTypography, + IconFileUnknown, + IconFileUpload, + IconFileVector, + IconFileX, + IconFileZip, + IconFiles, + IconFilesOff, + IconFilter, + IconFilterBolt, + IconFilterCancel, + IconFilterCheck, + IconFilterCode, + IconFilterCog, + IconFilterDiscount, + IconFilterDollar, + IconFilterDown, + IconFilterEdit, + IconFilterExclamation, + IconFilterHeart, + IconFilterMinus, + IconFilterOff, + IconFilterPause, + IconFilterPin, + IconFilterPlus, + IconFilterQuestion, + IconFilterSearch, + IconFilterShare, + IconFilterStar, + IconFilterUp, + IconFilterX, + IconFilters, + IconFingerprint, + IconFingerprintOff, + IconFireExtinguisher, + IconFireHydrant, + IconFireHydrantOff, + IconFiretruck, + IconFirstAidKit, + IconFirstAidKitOff, + IconFish, + IconFishBone, + IconFishChristianity, + IconFishHook, + IconFishHookOff, + IconFishOff, + IconFlag, + IconFlag2, + IconFlag2Off, + IconFlag3, + IconFlagBolt, + IconFlagCancel, + IconFlagCheck, + IconFlagCode, + IconFlagCog, + IconFlagDiscount, + IconFlagDollar, + IconFlagDown, + IconFlagExclamation, + IconFlagHeart, + IconFlagMinus, + IconFlagOff, + IconFlagPause, + IconFlagPin, + IconFlagPlus, + IconFlagQuestion, + IconFlagSearch, + IconFlagShare, + IconFlagStar, + IconFlagUp, + IconFlagX, + IconFlame, + IconFlameOff, + IconFlare, + IconFlask, + IconFlask2, + IconFlask2Off, + IconFlaskOff, + IconFlipFlops, + IconFlipHorizontal, + IconFlipVertical, + IconFloatCenter, + IconFloatLeft, + IconFloatNone, + IconFloatRight, + IconFlower, + IconFlowerOff, + IconFocus, + IconFocus2, + IconFocusAuto, + IconFocusCentered, + IconFold, + IconFoldDown, + IconFoldUp, + IconFolder, + IconFolderBolt, + IconFolderCancel, + IconFolderCheck, + IconFolderCode, + IconFolderCog, + IconFolderDollar, + IconFolderDown, + IconFolderExclamation, + IconFolderHeart, + IconFolderMinus, + IconFolderOff, + IconFolderOpen, + IconFolderPause, + IconFolderPin, + IconFolderPlus, + IconFolderQuestion, + IconFolderSearch, + IconFolderShare, + IconFolderStar, + IconFolderSymlink, + IconFolderUp, + IconFolderX, + IconFolders, + IconFoldersOff, + IconForbid, + IconForbid2, + IconForklift, + IconForms, + IconFountain, + IconFountainOff, + IconFrame, + IconFrameOff, + IconFreeRights, + IconFreezeColumn, + IconFreezeRow, + IconFreezeRowColumn, + IconFridge, + IconFridgeOff, + IconFriends, + IconFriendsOff, + IconFrustum, + IconFrustumOff, + IconFrustumPlus, + IconFunction, + IconFunctionOff, + IconGardenCart, + IconGardenCartOff, + IconGasStation, + IconGasStationOff, + IconGauge, + IconGaugeOff, + IconGavel, + IconGenderAgender, + IconGenderAndrogyne, + IconGenderBigender, + IconGenderDemiboy, + IconGenderDemigirl, + IconGenderEpicene, + IconGenderFemale, + IconGenderFemme, + IconGenderGenderfluid, + IconGenderGenderless, + IconGenderGenderqueer, + IconGenderHermaphrodite, + IconGenderIntergender, + IconGenderMale, + IconGenderNeutrois, + IconGenderThird, + IconGenderTransgender, + IconGenderTrasvesti, + IconGeometry, + IconGhost, + IconGhost2, + IconGhost3, + IconGhostOff, + IconGif, + IconGift, + IconGiftCard, + IconGiftOff, + IconGitBranch, + IconGitBranchDeleted, + IconGitCherryPick, + IconGitCommit, + IconGitCompare, + IconGitFork, + IconGitMerge, + IconGitPullRequest, + IconGitPullRequestClosed, + IconGitPullRequestDraft, + IconGizmo, + IconGlass, + IconGlassFull, + IconGlassOff, + IconGlobe, + IconGlobeOff, + IconGoGame, + IconGolf, + IconGolfOff, + IconGps, + IconGradienter, + IconGrain, + IconGraph, + IconGraphOff, + IconGrave, + IconGrave2, + IconGridDots, + IconGridPattern, + IconGrill, + IconGrillFork, + IconGrillOff, + IconGrillSpatula, + IconGripHorizontal, + IconGripVertical, + IconGrowth, + IconGuitarPick, + IconH1, + IconH2, + IconH3, + IconH4, + IconH5, + IconH6, + IconHammer, + IconHammerOff, + IconHandClick, + IconHandFinger, + IconHandFingerOff, + IconHandGrab, + IconHandLittleFinger, + IconHandMiddleFinger, + IconHandMove, + IconHandOff, + IconHandRingFinger, + IconHandSanitizer, + IconHandStop, + IconHandThreeFingers, + IconHandTwoFingers, + IconHanger, + IconHanger2, + IconHangerOff, + IconHash, + IconHaze, + IconHazeMoon, + IconHdr, + IconHeading, + IconHeadingOff, + IconHeadphones, + IconHeadphonesOff, + IconHeadset, + IconHeadsetOff, + IconHealthRecognition, + IconHeart, + IconHeartBolt, + IconHeartBroken, + IconHeartCancel, + IconHeartCheck, + IconHeartCode, + IconHeartCog, + IconHeartDiscount, + IconHeartDollar, + IconHeartDown, + IconHeartExclamation, + IconHeartHandshake, + IconHeartMinus, + IconHeartOff, + IconHeartPause, + IconHeartPin, + IconHeartPlus, + IconHeartQuestion, + IconHeartRateMonitor, + IconHeartSearch, + IconHeartShare, + IconHeartStar, + IconHeartUp, + IconHeartX, + IconHeartbeat, + IconHearts, + IconHeartsOff, + IconHelicopter, + IconHelicopterLanding, + IconHelmet, + IconHelmetOff, + IconHelp, + IconHelpCircle, + IconHelpHexagon, + IconHelpOctagon, + IconHelpOff, + IconHelpSmall, + IconHelpSquare, + IconHelpSquareRounded, + IconHelpTriangle, + IconHemisphere, + IconHemisphereOff, + IconHemispherePlus, + IconHexagon, + IconHexagon3d, + IconHexagonLetterA, + IconHexagonLetterB, + IconHexagonLetterC, + IconHexagonLetterD, + IconHexagonLetterE, + IconHexagonLetterF, + IconHexagonLetterG, + IconHexagonLetterH, + IconHexagonLetterI, + IconHexagonLetterJ, + IconHexagonLetterK, + IconHexagonLetterL, + IconHexagonLetterM, + IconHexagonLetterN, + IconHexagonLetterO, + IconHexagonLetterP, + IconHexagonLetterQ, + IconHexagonLetterR, + IconHexagonLetterS, + IconHexagonLetterT, + IconHexagonLetterU, + IconHexagonLetterV, + IconHexagonLetterW, + IconHexagonLetterX, + IconHexagonLetterY, + IconHexagonLetterZ, + IconHexagonNumber0, + IconHexagonNumber1, + IconHexagonNumber2, + IconHexagonNumber3, + IconHexagonNumber4, + IconHexagonNumber5, + IconHexagonNumber6, + IconHexagonNumber7, + IconHexagonNumber8, + IconHexagonNumber9, + IconHexagonOff, + IconHexagonalPrism, + IconHexagonalPrismOff, + IconHexagonalPrismPlus, + IconHexagonalPyramid, + IconHexagonalPyramidOff, + IconHexagonalPyramidPlus, + IconHexagons, + IconHexagonsOff, + IconHierarchy, + IconHierarchy2, + IconHierarchy3, + IconHierarchyOff, + IconHighlight, + IconHighlightOff, + IconHistory, + IconHistoryOff, + IconHistoryToggle, + IconHome, + IconHome2, + IconHomeBolt, + IconHomeCancel, + IconHomeCheck, + IconHomeCog, + IconHomeDollar, + IconHomeDot, + IconHomeDown, + IconHomeEco, + IconHomeEdit, + IconHomeExclamation, + IconHomeHand, + IconHomeHeart, + IconHomeInfinity, + IconHomeLink, + IconHomeMinus, + IconHomeMove, + IconHomeOff, + IconHomePlus, + IconHomeQuestion, + IconHomeRibbon, + IconHomeSearch, + IconHomeShare, + IconHomeShield, + IconHomeSignal, + IconHomeStar, + IconHomeStats, + IconHomeUp, + IconHomeX, + IconHorseToy, + IconHotelService, + IconHourglass, + IconHourglassEmpty, + IconHourglassHigh, + IconHourglassLow, + IconHourglassOff, + IconHours24, + IconHtml, + IconHttpConnect, + IconHttpDelete, + IconHttpGet, + IconHttpHead, + IconHttpOptions, + IconHttpPatch, + IconHttpPost, + IconHttpPut, + IconHttpQue, + IconHttpTrace, + IconIceCream, + IconIceCream2, + IconIceCreamOff, + IconIceSkating, + IconIcons, + IconIconsOff, + IconId, + IconIdBadge, + IconIdBadge2, + IconIdBadgeOff, + IconIdOff, + IconInbox, + IconInboxOff, + IconIndentDecrease, + IconIndentIncrease, + IconInfinity, + IconInfinityOff, + IconInfoCircle, + IconInfoHexagon, + IconInfoOctagon, + IconInfoSmall, + IconInfoSquare, + IconInfoSquareRounded, + IconInfoTriangle, + IconInnerShadowBottom, + IconInnerShadowBottomLeft, + IconInnerShadowBottomRight, + IconInnerShadowLeft, + IconInnerShadowRight, + IconInnerShadowTop, + IconInnerShadowTopLeft, + IconInnerShadowTopRight, + IconInputSearch, + IconIroning, + IconIroning1, + IconIroning2, + IconIroning3, + IconIroningOff, + IconIroningSteam, + IconIroningSteamOff, + IconIrregularPolyhedron, + IconIrregularPolyhedronOff, + IconIrregularPolyhedronPlus, + IconItalic, + IconJacket, + IconJetpack, + IconJewishStar, + IconJpg, + IconJson, + IconJumpRope, + IconKarate, + IconKayak, + IconKey, + IconKeyOff, + IconKeyboard, + IconKeyboardHide, + IconKeyboardOff, + IconKeyboardShow, + IconKeyframe, + IconKeyframeAlignCenter, + IconKeyframeAlignHorizontal, + IconKeyframeAlignVertical, + IconKeyframes, + IconLadder, + IconLadderOff, + IconLadle, + IconLambda, + IconLamp, + IconLamp2, + IconLampOff, + IconLane, + IconLanguage, + IconLanguageHiragana, + IconLanguageKatakana, + IconLanguageOff, + IconLasso, + IconLassoOff, + IconLassoPolygon, + IconLayersDifference, + IconLayersIntersect, + IconLayersIntersect2, + IconLayersLinked, + IconLayersOff, + IconLayersSubtract, + IconLayersUnion, + IconLayout, + IconLayout2, + IconLayoutAlignBottom, + IconLayoutAlignCenter, + IconLayoutAlignLeft, + IconLayoutAlignMiddle, + IconLayoutAlignRight, + IconLayoutAlignTop, + IconLayoutBoard, + IconLayoutBoardSplit, + IconLayoutBottombar, + IconLayoutBottombarCollapse, + IconLayoutBottombarExpand, + IconLayoutCards, + IconLayoutCollage, + IconLayoutColumns, + IconLayoutDashboard, + IconLayoutDistributeHorizontal, + IconLayoutDistributeVertical, + IconLayoutGrid, + IconLayoutGridAdd, + IconLayoutGridRemove, + IconLayoutKanban, + IconLayoutList, + IconLayoutNavbar, + IconLayoutNavbarCollapse, + IconLayoutNavbarExpand, + IconLayoutOff, + IconLayoutRows, + IconLayoutSidebar, + IconLayoutSidebarLeftCollapse, + IconLayoutSidebarLeftExpand, + IconLayoutSidebarRight, + IconLayoutSidebarRightCollapse, + IconLayoutSidebarRightExpand, + IconLeaf, + IconLeafOff, + IconLego, + IconLegoOff, + IconLemon, + IconLemon2, + IconLetterA, + IconLetterB, + IconLetterC, + IconLetterCase, + IconLetterCaseLower, + IconLetterCaseToggle, + IconLetterCaseUpper, + IconLetterD, + IconLetterE, + IconLetterF, + IconLetterG, + IconLetterH, + IconLetterI, + IconLetterJ, + IconLetterK, + IconLetterL, + IconLetterM, + IconLetterN, + IconLetterO, + IconLetterP, + IconLetterQ, + IconLetterR, + IconLetterS, + IconLetterSpacing, + IconLetterT, + IconLetterU, + IconLetterV, + IconLetterW, + IconLetterX, + IconLetterY, + IconLetterZ, + IconLicense, + IconLicenseOff, + IconLifebuoy, + IconLifebuoyOff, + IconLighter, + IconLine, + IconLineDashed, + IconLineDotted, + IconLineHeight, + IconLink, + IconLinkPlus, + IconLinkOff, + IconList, + IconListCheck, + IconListDetails, + IconListNumbers, + IconListSearch, + IconListTree, + IconLivePhoto, + IconLivePhotoOff, + IconLiveView, + IconLoadBalancer, + IconLoader, + IconLoader2, + IconLoader3, + IconLoaderQuarter, + IconLocation, + IconLocationBolt, + IconLocationBroken, + IconLocationCancel, + IconLocationCheck, + IconLocationCode, + IconLocationCog, + IconLocationDiscount, + IconLocationDollar, + IconLocationDown, + IconLocationExclamation, + IconLocationHeart, + IconLocationMinus, + IconLocationOff, + IconLocationPause, + IconLocationPin, + IconLocationPlus, + IconLocationQuestion, + IconLocationSearch, + IconLocationShare, + IconLocationStar, + IconLocationUp, + IconLocationX, + IconLock, + IconLockAccess, + IconLockAccessOff, + IconLockBolt, + IconLockCancel, + IconLockCheck, + IconLockCode, + IconLockCog, + IconLockDollar, + IconLockDown, + IconLockExclamation, + IconLockHeart, + IconLockMinus, + IconLockOff, + IconLockOpen, + IconLockOpenOff, + IconLockPause, + IconLockPin, + IconLockPlus, + IconLockQuestion, + IconLockSearch, + IconLockShare, + IconLockSquare, + IconLockSquareRounded, + IconLockStar, + IconLockUp, + IconLockX, + IconLogicAnd, + IconLogicBuffer, + IconLogicNand, + IconLogicNor, + IconLogicNot, + IconLogicOr, + IconLogicXnor, + IconLogicXor, + IconLogin, + IconLogout, + IconLogout2, + IconLollipop, + IconLollipopOff, + IconLuggage, + IconLuggageOff, + IconLungs, + IconLungsOff, + IconMacro, + IconMacroOff, + IconMagnet, + IconMagnetOff, + IconMail, + IconMailAi, + IconMailBolt, + IconMailCancel, + IconMailCheck, + IconMailCode, + IconMailCog, + IconMailDollar, + IconMailDown, + IconMailExclamation, + IconMailFast, + IconMailForward, + IconMailHeart, + IconMailMinus, + IconMailOff, + IconMailOpened, + IconMailPause, + IconMailPin, + IconMailPlus, + IconMailQuestion, + IconMailSearch, + IconMailShare, + IconMailStar, + IconMailUp, + IconMailX, + IconMailbox, + IconMailboxOff, + IconMan, + IconManualGearbox, + IconMap, + IconMap2, + IconMapBolt, + IconMapCancel, + IconMapCheck, + IconMapCode, + IconMapCog, + IconMapDiscount, + IconMapDollar, + IconMapDown, + IconMapExclamation, + IconMapHeart, + IconMapMinus, + IconMapOff, + IconMapPause, + IconMapPin, + IconMapPinBolt, + IconMapPinCancel, + IconMapPinCheck, + IconMapPinCode, + IconMapPinCog, + IconMapPinDollar, + IconMapPinDown, + IconMapPinExclamation, + IconMapPinHeart, + IconMapPinMinus, + IconMapPinOff, + IconMapPinPause, + IconMapPinPin, + IconMapPinPlus, + IconMapPinQuestion, + IconMapPinSearch, + IconMapPinShare, + IconMapPinStar, + IconMapPinUp, + IconMapPinX, + IconMapPins, + IconMapPlus, + IconMapQuestion, + IconMapSearch, + IconMapShare, + IconMapStar, + IconMapUp, + IconMapX, + IconMarkdown, + IconMarkdownOff, + IconMarquee, + IconMarquee2, + IconMarqueeOff, + IconMars, + IconMask, + IconMaskOff, + IconMasksTheater, + IconMasksTheaterOff, + IconMassage, + IconMatchstick, + IconMath, + IconMath1Divide2, + IconMath1Divide3, + IconMathAvg, + IconMathEqualGreater, + IconMathEqualLower, + IconMathFunction, + IconMathFunctionOff, + IconMathFunctionY, + IconMathGreater, + IconMathIntegral, + IconMathIntegralX, + IconMathIntegrals, + IconMathLower, + IconMathMax, + IconMathMin, + IconMathNot, + IconMathOff, + IconMathPi, + IconMathPiDivide2, + IconMathSymbols, + IconMathXDivide2, + IconMathXDivideY, + IconMathXDivideY2, + IconMathXMinusX, + IconMathXMinusY, + IconMathXPlusX, + IconMathXPlusY, + IconMathXy, + IconMathYMinusY, + IconMathYPlusY, + IconMaximize, + IconMaximizeOff, + IconMeat, + IconMeatOff, + IconMedal, + IconMedal2, + IconMedicalCross, + IconMedicalCrossCircle, + IconMedicalCrossOff, + IconMedicineSyrup, + IconMeeple, + IconMenorah, + IconMenu, + IconMenu2, + IconMenuDeep, + IconMenuOrder, + IconMessage, + IconMessage2, + IconMessage2Bolt, + IconMessage2Cancel, + IconMessage2Check, + IconMessage2Code, + IconMessage2Cog, + IconMessage2Dollar, + IconMessage2Down, + IconMessage2Exclamation, + IconMessage2Heart, + IconMessage2Minus, + IconMessage2Off, + IconMessage2Pause, + IconMessage2Pin, + IconMessage2Plus, + IconMessage2Question, + IconMessage2Search, + IconMessage2Share, + IconMessage2Star, + IconMessage2Up, + IconMessage2X, + IconMessageBolt, + IconMessageCancel, + IconMessageChatbot, + IconMessageCheck, + IconMessageCircle, + IconMessageCircleBolt, + IconMessageCircleCancel, + IconMessageCircleCheck, + IconMessageCircleCode, + IconMessageCircleCog, + IconMessageCircleDollar, + IconMessageCircleDown, + IconMessageCircleExclamation, + IconMessageCircleHeart, + IconMessageCircleMinus, + IconMessageCircleOff, + IconMessageCirclePause, + IconMessageCirclePin, + IconMessageCirclePlus, + IconMessageCircleQuestion, + IconMessageCircleSearch, + IconMessageCircleShare, + IconMessageCircleStar, + IconMessageCircleUp, + IconMessageCircleX, + IconMessageCode, + IconMessageCog, + IconMessageDollar, + IconMessageDots, + IconMessageDown, + IconMessageExclamation, + IconMessageForward, + IconMessageHeart, + IconMessageLanguage, + IconMessageMinus, + IconMessageOff, + IconMessagePause, + IconMessagePin, + IconMessagePlus, + IconMessageQuestion, + IconMessageReport, + IconMessageSearch, + IconMessageShare, + IconMessageStar, + IconMessageUp, + IconMessageX, + IconMessages, + IconMessagesOff, + IconMeteor, + IconMeteorOff, + IconMichelinBibGourmand, + IconMichelinStar, + IconMichelinStarGreen, + IconMickey, + IconMicrophone, + IconMicrophone2, + IconMicrophone2Off, + IconMicrophoneOff, + IconMicroscope, + IconMicroscopeOff, + IconMicrowave, + IconMicrowaveOff, + IconMilitaryAward, + IconMilitaryRank, + IconMilk, + IconMilkOff, + IconMilkshake, + IconMinimize, + IconMinus, + IconMist, + IconMistOff, + IconMobiledata, + IconMobiledataOff, + IconMoneybag, + IconMoodAngry, + IconMoodAnnoyed, + IconMoodAnnoyed2, + IconMoodBoy, + IconMoodCheck, + IconMoodCog, + IconMoodConfuzed, + IconMoodCrazyHappy, + IconMoodCry, + IconMoodDollar, + IconMoodEdit, + IconMoodEmpty, + IconMoodHappy, + IconMoodHeart, + IconMoodKid, + IconMoodLookLeft, + IconMoodLookRight, + IconMoodMinus, + IconMoodNerd, + IconMoodNervous, + IconMoodNeutral, + IconMoodOff, + IconMoodPin, + IconMoodPlus, + IconMoodSad, + IconMoodSad2, + IconMoodSadDizzy, + IconMoodSadSquint, + IconMoodSearch, + IconMoodShare, + IconMoodSick, + IconMoodSilence, + IconMoodSing, + IconMoodSmile, + IconMoodSmileBeam, + IconMoodSmileDizzy, + IconMoodTongue, + IconMoodTongueWink, + IconMoodTongueWink2, + IconMoodUnamused, + IconMoodUp, + IconMoodWink, + IconMoodWink2, + IconMoodWrrr, + IconMoodX, + IconMoodXd, + IconMoon, + IconMoon2, + IconMoonOff, + IconMoonStars, + IconMoped, + IconMotorbike, + IconMountain, + IconMountainOff, + IconMouse, + IconMouse2, + IconMouseOff, + IconMoustache, + IconMovie, + IconMovieOff, + IconMug, + IconMugOff, + IconMultiplier05x, + IconMultiplier15x, + IconMultiplier1x, + IconMultiplier2x, + IconMushroom, + IconMushroomOff, + IconMusic, + IconMusicBolt, + IconMusicCancel, + IconMusicCheck, + IconMusicCode, + IconMusicCog, + IconMusicDiscount, + IconMusicDollar, + IconMusicDown, + IconMusicExclamation, + IconMusicHeart, + IconMusicMinus, + IconMusicOff, + IconMusicPause, + IconMusicPin, + IconMusicPlus, + IconMusicQuestion, + IconMusicSearch, + IconMusicShare, + IconMusicStar, + IconMusicUp, + IconMusicX, + IconNavigation, + IconNavigationBolt, + IconNavigationCancel, + IconNavigationCheck, + IconNavigationCode, + IconNavigationCog, + IconNavigationDiscount, + IconNavigationDollar, + IconNavigationDown, + IconNavigationExclamation, + IconNavigationHeart, + IconNavigationMinus, + IconNavigationNorth, + IconNavigationOff, + IconNavigationPause, + IconNavigationPin, + IconNavigationPlus, + IconNavigationQuestion, + IconNavigationSearch, + IconNavigationShare, + IconNavigationStar, + IconNavigationUp, + IconNavigationX, + IconNeedle, + IconNeedleThread, + IconNetwork, + IconNetworkOff, + IconNewSection, + IconNews, + IconNewsOff, + IconNfc, + IconNfcOff, + IconNoCopyright, + IconNoCreativeCommons, + IconNoDerivatives, + IconNorthStar, + IconNote, + IconNoteOff, + IconNotebook, + IconNotebookOff, + IconNotes, + IconNotesOff, + IconNotification, + IconNotificationOff, + IconNumber, + IconNumber0, + IconNumber1, + IconNumber2, + IconNumber3, + IconNumber4, + IconNumber5, + IconNumber6, + IconNumber7, + IconNumber8, + IconNumber9, + IconNumbers, + IconNurse, + IconOctagon, + IconOctagonOff, + IconOctahedron, + IconOctahedronOff, + IconOctahedronPlus, + IconOld, + IconOlympics, + IconOlympicsOff, + IconOm, + IconOmega, + IconOutbound, + IconOutlet, + IconOval, + IconOvalVertical, + IconOverline, + IconPackage, + IconPackageExport, + IconPackageImport, + IconPackageOff, + IconPackages, + IconPacman, + IconPageBreak, + IconPaint, + IconPaintOff, + IconPalette, + IconPaletteOff, + IconPanoramaHorizontal, + IconPanoramaHorizontalOff, + IconPanoramaVertical, + IconPanoramaVerticalOff, + IconPaperBag, + IconPaperBagOff, + IconPaperclip, + IconParachute, + IconParachuteOff, + IconParentheses, + IconParenthesesOff, + IconParking, + IconParkingOff, + IconPassword, + IconPaw, + IconPawOff, + IconPdf, + IconPeace, + IconPencil, + IconPencilBolt, + IconPencilCancel, + IconPencilCheck, + IconPencilCode, + IconPencilCog, + IconPencilDiscount, + IconPencilDollar, + IconPencilDown, + IconPencilExclamation, + IconPencilHeart, + IconPencilMinus, + IconPencilOff, + IconPencilPause, + IconPencilPin, + IconPencilPlus, + IconPencilQuestion, + IconPencilSearch, + IconPencilShare, + IconPencilStar, + IconPencilUp, + IconPencilX, + IconPennant, + IconPennant2, + IconPennantOff, + IconPentagon, + IconPentagonOff, + IconPentagram, + IconPepper, + IconPepperOff, + IconPercentage, + IconPerfume, + IconPerspective, + IconPerspectiveOff, + IconPhone, + IconPhoneCall, + IconPhoneCalling, + IconPhoneCheck, + IconPhoneIncoming, + IconPhoneOff, + IconPhoneOutgoing, + IconPhonePause, + IconPhonePlus, + IconPhoneX, + IconPhoto, + IconPhotoAi, + IconPhotoBolt, + IconPhotoCancel, + IconPhotoCheck, + IconPhotoCode, + IconPhotoCog, + IconPhotoDollar, + IconPhotoDown, + IconPhotoEdit, + IconPhotoExclamation, + IconPhotoHeart, + IconPhotoMinus, + IconPhotoOff, + IconPhotoPause, + IconPhotoPin, + IconPhotoPlus, + IconPhotoQuestion, + IconPhotoSearch, + IconPhotoSensor, + IconPhotoSensor2, + IconPhotoSensor3, + IconPhotoShare, + IconPhotoShield, + IconPhotoStar, + IconPhotoUp, + IconPhotoX, + IconPhysotherapist, + IconPiano, + IconPick, + IconPictureInPicture, + IconPictureInPictureOff, + IconPictureInPictureOn, + IconPictureInPictureTop, + IconPig, + IconPigMoney, + IconPigOff, + IconPilcrow, + IconPill, + IconPillOff, + IconPills, + IconPin, + IconPingPong, + IconPinned, + IconPinnedOff, + IconPizza, + IconPizzaOff, + IconPlaceholder, + IconPlane, + IconPlaneArrival, + IconPlaneDeparture, + IconPlaneInflight, + IconPlaneOff, + IconPlaneTilt, + IconPlanet, + IconPlanetOff, + IconPlant, + IconPlant2, + IconPlant2Off, + IconPlantOff, + IconPlayBasketball, + IconPlayCard, + IconPlayCardOff, + IconPlayFootball, + IconPlayHandball, + IconPlayVolleyball, + IconPlayerEject, + IconPlayerPause, + IconPlayerPlay, + IconPlayerRecord, + IconPlayerSkipBack, + IconPlayerSkipForward, + IconPlayerStop, + IconPlayerTrackNext, + IconPlayerTrackPrev, + IconPlaylist, + IconPlaylistAdd, + IconPlaylistOff, + IconPlaylistX, + IconPlaystationCircle, + IconPlaystationSquare, + IconPlaystationTriangle, + IconPlaystationX, + IconPlug, + IconPlugConnected, + IconPlugConnectedX, + IconPlugOff, + IconPlugX, + IconPlus, + IconPlusEqual, + IconPlusMinus, + IconPng, + IconPodium, + IconPodiumOff, + IconPoint, + IconPointOff, + IconPointer, + IconPointerBolt, + IconPointerCancel, + IconPointerCheck, + IconPointerCode, + IconPointerCog, + IconPointerDollar, + IconPointerDown, + IconPointerExclamation, + IconPointerHeart, + IconPointerMinus, + IconPointerOff, + IconPointerPause, + IconPointerPin, + IconPointerPlus, + IconPointerQuestion, + IconPointerSearch, + IconPointerShare, + IconPointerStar, + IconPointerUp, + IconPointerX, + IconPokeball, + IconPokeballOff, + IconPokerChip, + IconPolaroid, + IconPolygon, + IconPolygonOff, + IconPoo, + IconPool, + IconPoolOff, + IconPower, + IconPray, + IconPremiumRights, + IconPrescription, + IconPresentation, + IconPresentationAnalytics, + IconPresentationOff, + IconPrinter, + IconPrinterOff, + IconPrism, + IconPrismOff, + IconPrismPlus, + IconPrison, + IconProgress, + IconProgressAlert, + IconProgressBolt, + IconProgressCheck, + IconProgressDown, + IconProgressHelp, + IconProgressX, + IconPrompt, + IconPropeller, + IconPropellerOff, + IconPumpkinScary, + IconPuzzle, + IconPuzzle2, + IconPuzzleOff, + IconPyramid, + IconPyramidOff, + IconPyramidPlus, + IconQrcode, + IconQrcodeOff, + IconQuestionMark, + IconQuote, + IconQuoteOff, + IconQuotes, + IconRadar, + IconRadar2, + IconRadarOff, + IconRadio, + IconRadioOff, + IconRadioactive, + IconRadioactiveOff, + IconRadiusBottomLeft, + IconRadiusBottomRight, + IconRadiusTopLeft, + IconRadiusTopRight, + IconRainbow, + IconRainbowOff, + IconRating12Plus, + IconRating14Plus, + IconRating16Plus, + IconRating18Plus, + IconRating21Plus, + IconRazor, + IconRazorElectric, + IconReceipt, + IconReceipt2, + IconReceiptOff, + IconReceiptRefund, + IconReceiptTax, + IconRecharging, + IconRecordMail, + IconRecordMailOff, + IconRectangle, + IconRectangleRoundedBottom, + IconRectangleRoundedTop, + IconRectangleVertical, + IconRectangularPrism, + IconRectangularPrismOff, + IconRectangularPrismPlus, + IconRecycle, + IconRecycleOff, + IconRefresh, + IconRefreshAlert, + IconRefreshDot, + IconRefreshOff, + IconRegex, + IconRegexOff, + IconRegistered, + IconRelationManyToMany, + IconRelationOneToMany, + IconRelationOneToOne, + IconReload, + IconReorder, + IconRepeat, + IconRepeatOff, + IconRepeatOnce, + IconReplace, + IconReplaceOff, + IconReport, + IconReportAnalytics, + IconReportMedical, + IconReportMoney, + IconReportOff, + IconReportSearch, + IconReservedLine, + IconResize, + IconRestore, + IconRewindBackward10, + IconRewindBackward15, + IconRewindBackward20, + IconRewindBackward30, + IconRewindBackward40, + IconRewindBackward5, + IconRewindBackward50, + IconRewindBackward60, + IconRewindForward10, + IconRewindForward15, + IconRewindForward20, + IconRewindForward30, + IconRewindForward40, + IconRewindForward5, + IconRewindForward50, + IconRewindForward60, + IconRibbonHealth, + IconRings, + IconRipple, + IconRippleOff, + IconRoad, + IconRoadOff, + IconRoadSign, + IconRobot, + IconRobotOff, + IconRocket, + IconRocketOff, + IconRollerSkating, + IconRollercoaster, + IconRollercoasterOff, + IconRosette, + IconRosetteNumber0, + IconRosetteNumber1, + IconRosetteNumber2, + IconRosetteNumber3, + IconRosetteNumber4, + IconRosetteNumber5, + IconRosetteNumber6, + IconRosetteNumber7, + IconRosetteNumber8, + IconRosetteNumber9, + IconRotate, + IconRotate2, + IconRotate360, + IconRotate3d, + IconRotateClockwise, + IconRotateClockwise2, + IconRotateDot, + IconRotateRectangle, + IconRoute, + IconRoute2, + IconRouteOff, + IconRouter, + IconRouterOff, + IconRowInsertBottom, + IconRowInsertTop, + IconRowRemove, + IconRss, + IconRubberStamp, + IconRubberStampOff, + IconRuler, + IconRuler2, + IconRuler2Off, + IconRuler3, + IconRulerMeasure, + IconRulerOff, + IconRun, + IconSTurnDown, + IconSTurnLeft, + IconSTurnRight, + IconSTurnUp, + IconSailboat, + IconSailboat2, + IconSailboatOff, + IconSalad, + IconSalt, + IconSatellite, + IconSatelliteOff, + IconSausage, + IconScale, + IconScaleOff, + IconScaleOutline, + IconScaleOutlineOff, + IconScan, + IconScanEye, + IconSchema, + IconSchemaOff, + IconSchool, + IconSchoolBell, + IconSchoolOff, + IconScissors, + IconScissorsOff, + IconScooter, + IconScooterElectric, + IconScoreboard, + IconScreenShare, + IconScreenShareOff, + IconScreenshot, + IconScribble, + IconScribbleOff, + IconScript, + IconScriptMinus, + IconScriptPlus, + IconScriptX, + IconScubaMask, + IconScubaMaskOff, + IconSdk, + IconSearch, + IconSearchOff, + IconSection, + IconSectionSign, + IconSelect, + IconSelectAll, + IconSelector, + IconSend, + IconSendOff, + IconSeo, + IconSeparator, + IconSeparatorHorizontal, + IconSeparatorVertical, + IconServer, + IconServer2, + IconServerBolt, + IconServerCog, + IconServerOff, + IconServicemark, + IconSettings, + IconSettings2, + IconSettingsAutomation, + IconSettingsBolt, + IconSettingsCancel, + IconSettingsCheck, + IconSettingsCode, + IconSettingsCog, + IconSettingsDollar, + IconSettingsDown, + IconSettingsExclamation, + IconSettingsHeart, + IconSettingsMinus, + IconSettingsOff, + IconSettingsPause, + IconSettingsPin, + IconSettingsPlus, + IconSettingsQuestion, + IconSettingsSearch, + IconSettingsShare, + IconSettingsStar, + IconSettingsUp, + IconSettingsX, + IconShadow, + IconShadowOff, + IconShape, + IconShape2, + IconShape3, + IconShapeOff, + IconShare, + IconShare2, + IconShare3, + IconShareOff, + IconShield, + IconShieldBolt, + IconShieldCancel, + IconShieldCheck, + IconShieldCheckered, + IconShieldChevron, + IconShieldCode, + IconShieldCog, + IconShieldDollar, + IconShieldDown, + IconShieldExclamation, + IconShieldHalf, + IconShieldHeart, + IconShieldLock, + IconShieldMinus, + IconShieldOff, + IconShieldPause, + IconShieldPin, + IconShieldPlus, + IconShieldQuestion, + IconShieldSearch, + IconShieldShare, + IconShieldStar, + IconShieldUp, + IconShieldX, + IconShip, + IconShipOff, + IconShirt, + IconShirtOff, + IconShirtSport, + IconShoe, + IconShoeOff, + IconShoppingBag, + IconShoppingBagCheck, + IconShoppingBagDiscount, + IconShoppingBagEdit, + IconShoppingBagExclamation, + IconShoppingBagMinus, + IconShoppingBagPlus, + IconShoppingBagSearch, + IconShoppingBagX, + IconShoppingCart, + IconShoppingCartBolt, + IconShoppingCartCancel, + IconShoppingCartCheck, + IconShoppingCartCode, + IconShoppingCartCog, + IconShoppingCartCopy, + IconShoppingCartDiscount, + IconShoppingCartDollar, + IconShoppingCartDown, + IconShoppingCartExclamation, + IconShoppingCartHeart, + IconShoppingCartMinus, + IconShoppingCartOff, + IconShoppingCartPause, + IconShoppingCartPin, + IconShoppingCartPlus, + IconShoppingCartQuestion, + IconShoppingCartSearch, + IconShoppingCartShare, + IconShoppingCartStar, + IconShoppingCartUp, + IconShoppingCartX, + IconShovel, + IconShredder, + IconSignLeft, + IconSignRight, + IconSignal2g, + IconSignal3g, + IconSignal4g, + IconSignal4gPlus, + IconSignal5g, + IconSignal6g, + IconSignalE, + IconSignalG, + IconSignalH, + IconSignalHPlus, + IconSignalLte, + IconSignature, + IconSignatureOff, + IconSitemap, + IconSitemapOff, + IconSkateboard, + IconSkateboardOff, + IconSkateboarding, + IconSkull, + IconSlash, + IconSlashes, + IconSleigh, + IconSlice, + IconSlideshow, + IconSmartHome, + IconSmartHomeOff, + IconSmoking, + IconSmokingNo, + IconSnowflake, + IconSnowflakeOff, + IconSnowman, + IconSoccerField, + IconSocial, + IconSocialOff, + IconSock, + IconSofa, + IconSofaOff, + IconSolarPanel, + IconSolarPanel2, + IconSort09, + IconSort90, + IconSortAZ, + IconSortAscending, + IconSortAscending2, + IconSortAscendingLetters, + IconSortAscendingNumbers, + IconSortDescending, + IconSortDescending2, + IconSortDescendingLetters, + IconSortDescendingNumbers, + IconSortZA, + IconSos, + IconSoup, + IconSoupOff, + IconSourceCode, + IconSpace, + IconSpaceOff, + IconSpacingHorizontal, + IconSpacingVertical, + IconSpade, + IconSparkles, + IconSpeakerphone, + IconSpeedboat, + IconSphere, + IconSphereOff, + IconSpherePlus, + IconSpider, + IconSpiral, + IconSpiralOff, + IconSportBillard, + IconSpray, + IconSpy, + IconSpyOff, + IconSql, + IconSquare, + IconSquareArrowDown, + IconSquareArrowLeft, + IconSquareArrowRight, + IconSquareArrowUp, + IconSquareAsterisk, + IconSquareCheck, + IconSquareChevronDown, + IconSquareChevronLeft, + IconSquareChevronRight, + IconSquareChevronUp, + IconSquareChevronsDown, + IconSquareChevronsLeft, + IconSquareChevronsRight, + IconSquareChevronsUp, + IconSquareDot, + IconSquareF0, + IconSquareF1, + IconSquareF2, + IconSquareF3, + IconSquareF4, + IconSquareF5, + IconSquareF6, + IconSquareF7, + IconSquareF8, + IconSquareF9, + IconSquareForbid, + IconSquareForbid2, + IconSquareHalf, + IconSquareKey, + IconSquareLetterA, + IconSquareLetterB, + IconSquareLetterC, + IconSquareLetterD, + IconSquareLetterE, + IconSquareLetterF, + IconSquareLetterG, + IconSquareLetterH, + IconSquareLetterI, + IconSquareLetterJ, + IconSquareLetterK, + IconSquareLetterL, + IconSquareLetterM, + IconSquareLetterN, + IconSquareLetterO, + IconSquareLetterP, + IconSquareLetterQ, + IconSquareLetterR, + IconSquareLetterS, + IconSquareLetterT, + IconSquareLetterU, + IconSquareLetterV, + IconSquareLetterW, + IconSquareLetterX, + IconSquareLetterY, + IconSquareLetterZ, + IconSquareMinus, + IconSquareNumber0, + IconSquareNumber1, + IconSquareNumber2, + IconSquareNumber3, + IconSquareNumber4, + IconSquareNumber5, + IconSquareNumber6, + IconSquareNumber7, + IconSquareNumber8, + IconSquareNumber9, + IconSquareOff, + IconSquarePlus, + IconSquareRoot, + IconSquareRoot2, + IconSquareRotated, + IconSquareRotatedForbid, + IconSquareRotatedForbid2, + IconSquareRotatedOff, + IconSquareRounded, + IconSquareRoundedArrowDown, + IconSquareRoundedArrowLeft, + IconSquareRoundedArrowRight, + IconSquareRoundedArrowUp, + IconSquareRoundedCheck, + IconSquareRoundedChevronDown, + IconSquareRoundedChevronLeft, + IconSquareRoundedChevronRight, + IconSquareRoundedChevronUp, + IconSquareRoundedChevronsDown, + IconSquareRoundedChevronsLeft, + IconSquareRoundedChevronsRight, + IconSquareRoundedChevronsUp, + IconSquareRoundedLetterA, + IconSquareRoundedLetterB, + IconSquareRoundedLetterC, + IconSquareRoundedLetterD, + IconSquareRoundedLetterE, + IconSquareRoundedLetterF, + IconSquareRoundedLetterG, + IconSquareRoundedLetterH, + IconSquareRoundedLetterI, + IconSquareRoundedLetterJ, + IconSquareRoundedLetterK, + IconSquareRoundedLetterL, + IconSquareRoundedLetterM, + IconSquareRoundedLetterN, + IconSquareRoundedLetterO, + IconSquareRoundedLetterP, + IconSquareRoundedLetterQ, + IconSquareRoundedLetterR, + IconSquareRoundedLetterS, + IconSquareRoundedLetterT, + IconSquareRoundedLetterU, + IconSquareRoundedLetterV, + IconSquareRoundedLetterW, + IconSquareRoundedLetterX, + IconSquareRoundedLetterY, + IconSquareRoundedLetterZ, + IconSquareRoundedMinus, + IconSquareRoundedNumber0, + IconSquareRoundedNumber1, + IconSquareRoundedNumber2, + IconSquareRoundedNumber3, + IconSquareRoundedNumber4, + IconSquareRoundedNumber5, + IconSquareRoundedNumber6, + IconSquareRoundedNumber7, + IconSquareRoundedNumber8, + IconSquareRoundedNumber9, + IconSquareRoundedPlus, + IconSquareRoundedX, + IconSquareToggle, + IconSquareToggleHorizontal, + IconSquareX, + IconSquaresDiagonal, + IconStack, + IconStack2, + IconStack3, + IconStackPop, + IconStackPush, + IconStairs, + IconStairsDown, + IconStairsUp, + IconStar, + IconStarHalf, + IconStarOff, + IconStars, + IconStarsOff, + IconStatusChange, + IconSteam, + IconSteeringWheel, + IconSteeringWheelOff, + IconStepInto, + IconStepOut, + IconStereoGlasses, + IconStethoscope, + IconStethoscopeOff, + IconSticker, + IconStorm, + IconStormOff, + IconStretching, + IconStretching2, + IconStrikethrough, + IconSubmarine, + IconSubscript, + IconSubtask, + IconSum, + IconSumOff, + IconSun, + IconSunHigh, + IconSunLow, + IconSunMoon, + IconSunOff, + IconSunWind, + IconSunglasses, + IconSunrise, + IconSunset, + IconSunset2, + IconSuperscript, + IconSvg, + IconSwimming, + IconSwipe, + IconSwitch, + IconSwitch2, + IconSwitch3, + IconSwitchHorizontal, + IconSwitchVertical, + IconSword, + IconSwordOff, + IconSwords, + IconTable, + IconTableAlias, + IconTableColumn, + IconTableDown, + IconTableExport, + IconTableHeart, + IconTableImport, + IconTableMinus, + IconTableOff, + IconTableOptions, + IconTablePlus, + IconTableRow, + IconTableShare, + IconTableShortcut, + IconTag, + IconTagOff, + IconTags, + IconTagsOff, + IconTallymark1, + IconTallymark2, + IconTallymark3, + IconTallymark4, + IconTallymarks, + IconTank, + IconTarget, + IconTargetArrow, + IconTargetOff, + IconTeapot, + IconTelescope, + IconTelescopeOff, + IconTemperature, + IconTemperatureCelsius, + IconTemperatureFahrenheit, + IconTemperatureMinus, + IconTemperatureOff, + IconTemperaturePlus, + IconTemplate, + IconTemplateOff, + IconTent, + IconTentOff, + IconTerminal, + IconTerminal2, + IconTestPipe, + IconTestPipe2, + IconTestPipeOff, + IconTex, + IconTextCaption, + IconTextColor, + IconTextDecrease, + IconTextDirectionLtr, + IconTextDirectionRtl, + IconTextIncrease, + IconTextOrientation, + IconTextPlus, + IconTextRecognition, + IconTextResize, + IconTextSize, + IconTextSpellcheck, + IconTextWrap, + IconTextWrapDisabled, + IconTexture, + IconTheater, + IconThermometer, + IconThumbDown, + IconThumbDownOff, + IconThumbUp, + IconThumbUpOff, + IconTicTac, + IconTicket, + IconTicketOff, + IconTie, + IconTilde, + IconTiltShift, + IconTiltShiftOff, + IconTimeDuration0, + IconTimeDuration10, + IconTimeDuration15, + IconTimeDuration30, + IconTimeDuration45, + IconTimeDuration5, + IconTimeDuration60, + IconTimeDuration90, + IconTimeDurationOff, + IconTimeline, + IconTimelineEvent, + IconTimelineEventExclamation, + IconTimelineEventMinus, + IconTimelineEventPlus, + IconTimelineEventText, + IconTimelineEventX, + IconTir, + IconToggleLeft, + IconToggleRight, + IconToiletPaper, + IconToiletPaperOff, + IconToml, + IconTool, + IconTools, + IconToolsKitchen, + IconToolsKitchen2, + IconToolsKitchen2Off, + IconToolsKitchenOff, + IconToolsOff, + IconTooltip, + IconTopologyBus, + IconTopologyComplex, + IconTopologyFull, + IconTopologyFullHierarchy, + IconTopologyRing, + IconTopologyRing2, + IconTopologyRing3, + IconTopologyStar, + IconTopologyStar2, + IconTopologyStar3, + IconTopologyStarRing, + IconTopologyStarRing2, + IconTopologyStarRing3, + IconTorii, + IconTornado, + IconTournament, + IconTower, + IconTowerOff, + IconTrack, + IconTractor, + IconTrademark, + IconTrafficCone, + IconTrafficConeOff, + IconTrafficLights, + IconTrafficLightsOff, + IconTrain, + IconTransfer, + IconTransferIn, + IconTransferOut, + IconTransferVertical, + IconTransform, + IconTransitionBottom, + IconTransitionLeft, + IconTransitionRight, + IconTransitionTop, + IconTrash, + IconTrashOff, + IconTrashX, + IconTreadmill, + IconTree, + IconTrees, + IconTrekking, + IconTrendingDown, + IconTrendingDown2, + IconTrendingDown3, + IconTrendingUp, + IconTrendingUp2, + IconTrendingUp3, + IconTriangle, + IconTriangleInverted, + IconTriangleOff, + IconTriangleSquareCircle, + IconTriangles, + IconTrident, + IconTrolley, + IconTrophy, + IconTrophyOff, + IconTrowel, + IconTruck, + IconTruckDelivery, + IconTruckLoading, + IconTruckOff, + IconTruckReturn, + IconTxt, + IconTypography, + IconTypographyOff, + IconUfo, + IconUfoOff, + IconUmbrella, + IconUmbrellaOff, + IconUnderline, + IconUnlink, + IconUpload, + IconUrgent, + IconUsb, + IconUser, + IconUserBolt, + IconUserCancel, + IconUserCheck, + IconUserCircle, + IconUserCode, + IconUserCog, + IconUserDollar, + IconUserDown, + IconUserEdit, + IconUserExclamation, + IconUserHeart, + IconUserMinus, + IconUserOff, + IconUserPause, + IconUserPin, + IconUserPlus, + IconUserQuestion, + IconUserSearch, + IconUserShare, + IconUserShield, + IconUserStar, + IconUserUp, + IconUserX, + IconUsers, + IconUsersGroup, + IconUsersMinus, + IconUsersPlus, + IconUvIndex, + IconUxCircle, + IconVaccine, + IconVaccineBottle, + IconVaccineBottleOff, + IconVaccineOff, + IconVacuumCleaner, + IconVariable, + IconVariableMinus, + IconVariableOff, + IconVariablePlus, + IconVector, + IconVectorBezier, + IconVectorBezier2, + IconVectorBezierArc, + IconVectorBezierCircle, + IconVectorOff, + IconVectorSpline, + IconVectorTriangle, + IconVectorTriangleOff, + IconVenus, + IconVersions, + IconVersionsOff, + IconVideo, + IconVideoMinus, + IconVideoOff, + IconVideoPlus, + IconView360, + IconView360Off, + IconViewfinder, + IconViewfinderOff, + IconViewportNarrow, + IconViewportWide, + IconVinyl, + IconVip, + IconVipOff, + IconVirus, + IconVirusOff, + IconVirusSearch, + IconVocabulary, + IconVocabularyOff, + IconVolcano, + IconVolume, + IconVolume2, + IconVolume3, + IconVolumeOff, + IconWalk, + IconWall, + IconWallOff, + IconWallet, + IconWalletOff, + IconWallpaper, + IconWallpaperOff, + IconWand, + IconWandOff, + IconWash, + IconWashDry, + IconWashDry1, + IconWashDry2, + IconWashDry3, + IconWashDryA, + IconWashDryDip, + IconWashDryF, + IconWashDryFlat, + IconWashDryHang, + IconWashDryOff, + IconWashDryP, + IconWashDryShade, + IconWashDryW, + IconWashDryclean, + IconWashDrycleanOff, + IconWashEco, + IconWashGentle, + IconWashHand, + IconWashMachine, + IconWashOff, + IconWashPress, + IconWashTemperature1, + IconWashTemperature2, + IconWashTemperature3, + IconWashTemperature4, + IconWashTemperature5, + IconWashTemperature6, + IconWashTumbleDry, + IconWashTumbleOff, + IconWaterpolo, + IconWaveSawTool, + IconWaveSine, + IconWaveSquare, + IconWebhook, + IconWebhookOff, + IconWeight, + IconWheelchair, + IconWheelchairOff, + IconWhirl, + IconWifi, + IconWifi0, + IconWifi1, + IconWifi2, + IconWifiOff, + IconWind, + IconWindOff, + IconWindow, + IconWindowMaximize, + IconWindowMinimize, + IconWindowOff, + IconWindsock, + IconWiper, + IconWiperWash, + IconWoman, + IconWood, + IconWorld, + IconWorldBolt, + IconWorldCancel, + IconWorldCheck, + IconWorldCode, + IconWorldCog, + IconWorldDollar, + IconWorldDown, + IconWorldDownload, + IconWorldExclamation, + IconWorldHeart, + IconWorldLatitude, + IconWorldLongitude, + IconWorldMinus, + IconWorldOff, + IconWorldPause, + IconWorldPin, + IconWorldPlus, + IconWorldQuestion, + IconWorldSearch, + IconWorldShare, + IconWorldStar, + IconWorldUp, + IconWorldUpload, + IconWorldWww, + IconWorldX, + IconWreckingBall, + IconWriting, + IconWritingOff, + IconWritingSign, + IconWritingSignOff, + IconX, + IconXboxA, + IconXboxB, + IconXboxX, + IconXboxY, + IconXd, + IconXxx, + IconYinYang, + IconYoga, + IconZeppelin, + IconZeppelinOff, + IconZip, + IconZodiacAquarius, + IconZodiacAries, + IconZodiacCancer, + IconZodiacCapricorn, + IconZodiacGemini, + IconZodiacLeo, + IconZodiacLibra, + IconZodiacPisces, + IconZodiacSagittarius, + IconZodiacScorpio, + IconZodiacTaurus, + IconZodiacVirgo, + IconZoomCancel, + IconZoomCheck, + IconZoomCode, + IconZoomExclamation, + IconZoomIn, + IconZoomInArea, + IconZoomMoney, + IconZoomOut, + IconZoomOutArea, + IconZoomPan, + IconZoomQuestion, + IconZoomReplace, + IconZoomReset, + IconZzz, + IconZzzOff, + IconCancel, + IconBadge2k, +}; diff --git a/packages/twenty-ui/src/display/icon/states/iconsState.ts b/packages/twenty-ui/src/display/icon/states/iconsState.ts new file mode 100644 index 0000000000..4a88b2cf07 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/states/iconsState.ts @@ -0,0 +1,6 @@ +import { atom } from 'jotai'; + +import { type IconComponent } from '@ui/display/icon/types/IconComponent'; + +export const iconsState = atom>({}); +iconsState.debugLabel = 'iconsState'; diff --git a/packages/twenty-ui/src/display/icon/types/IconComponent.ts b/packages/twenty-ui/src/display/icon/types/IconComponent.ts new file mode 100644 index 0000000000..1e38c862b0 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/types/IconComponent.ts @@ -0,0 +1,11 @@ +import type { CSSProperties, FunctionComponent } from 'react'; + +export type IconComponentProps = { + className?: string; + style?: CSSProperties; + size?: number | string; + stroke?: number | string; + color?: string; +}; + +export type IconComponent = FunctionComponent; diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts index a3046804c0..64d3edbed5 100644 --- a/packages/twenty-ui/src/display/index.ts +++ b/packages/twenty-ui/src/display/index.ts @@ -7,4 +7,542 @@ * |___/ */ -export {}; +export type { AvatarProps } from './avatar/components/Avatar'; +export { Avatar } from './avatar/components/Avatar'; +export type { AvatarGroupProps } from './avatar/components/AvatarGroup'; +export { AvatarGroup } from './avatar/components/AvatarGroup'; +export { invalidAvatarUrlsAtomV2 } from './avatar/components/states/invalidAvatarUrlsAtomV2'; +export { AVATAR_PROPERTIES_BY_SIZE } from './avatar/constants/AvatarPropertiesBySize'; +export type { AvatarSize } from './avatar/types/AvatarSize'; +export type { AvatarType } from './avatar/types/AvatarType'; +export type { BannerColor, BannerVariant } from './banner/components/Banner'; +export { Banner } from './banner/components/Banner'; +export { InlineBanner } from './banner/components/InlineBanner'; +export type { SidePanelInformationBannerProps } from './banner/components/SidePanelInformationBanner'; +export { SidePanelInformationBanner } from './banner/components/SidePanelInformationBanner'; +export type { CalloutVariant, CalloutProps } from './callout/Callout'; +export { Callout } from './callout/Callout'; +export type { AnimatedCheckmarkProps } from './checkmark/components/AnimatedCheckmark'; +export { AnimatedCheckmark } from './checkmark/components/AnimatedCheckmark'; +export type { CheckmarkProps } from './checkmark/components/Checkmark'; +export { Checkmark } from './checkmark/components/Checkmark'; +export type { + ColorSampleVariant, + ColorSampleProps, +} from './color/components/ColorSample'; +export { ColorSample } from './color/components/ColorSample'; +export { CommandBlock } from './command-block/components/CommandBlock'; +export { LinkifiedText } from './components/LinkifiedText'; +export type { IconProps } from './icon/components/Icon'; +export { Icon } from './icon/components/Icon'; +export { IconAddressBook } from './icon/components/IconAddressBook'; +export { IconBrandAnthropic } from './icon/components/IconBrandAnthropic'; +export { IconBrandGemini } from './icon/components/IconBrandGemini'; +export { IconBrandGroq } from './icon/components/IconBrandGroq'; +export { IconBrandMistral } from './icon/components/IconBrandMistral'; +export { IconBrandXai } from './icon/components/IconBrandXai'; +export { IconChartBarHorizontal } from './icon/components/IconChartBarHorizontal'; +export { IconGmail } from './icon/components/IconGmail'; +export { IconGoogle } from './icon/components/IconGoogle'; +export { IconGoogleCalendar } from './icon/components/IconGoogleCalendar'; +export { IconLockCustom } from './icon/components/IconLock'; +export { IconMicrosoft } from './icon/components/IconMicrosoft'; +export { IconMicrosoftCalendar } from './icon/components/IconMicrosoftCalendar'; +export { IconMicrosoftOutlook } from './icon/components/IconMicrosoftOutlook'; +export { IconModelClaude } from './icon/components/IconModelClaude'; +export { IconProviderOpenai } from './icon/components/IconProviderOpenai'; +export { IconRelationManyToOne } from './icon/components/IconRelationManyToOne'; +export { IconTrashXOff } from './icon/components/IconTrashXOff'; +export { IconTwentyStar } from './icon/components/IconTwentyStar'; +export { IconTwentyStarFilled } from './icon/components/IconTwentyStarFilled'; +export { IllustrationIconArray } from './icon/components/IllustrationIconArray'; +export { IllustrationIconCalendarEvent } from './icon/components/IllustrationIconCalendarEvent'; +export { IllustrationIconCalendarTime } from './icon/components/IllustrationIconCalendarTime'; +export { IllustrationIconCurrency } from './icon/components/IllustrationIconCurrency'; +export { IllustrationIconFile } from './icon/components/IllustrationIconFile'; +export { IllustrationIconJson } from './icon/components/IllustrationIconJson'; +export { IllustrationIconLink } from './icon/components/IllustrationIconLink'; +export { IllustrationIconMail } from './icon/components/IllustrationIconMail'; +export { IllustrationIconManyToMany } from './icon/components/IllustrationIconManyToMany'; +export { IllustrationIconMap } from './icon/components/IllustrationIconMap'; +export { IllustrationIconNumbers } from './icon/components/IllustrationIconNumbers'; +export { IllustrationIconOneToMany } from './icon/components/IllustrationIconOneToMany'; +export { IllustrationIconOneToOne } from './icon/components/IllustrationIconOneToOne'; +export { IllustrationIconPhone } from './icon/components/IllustrationIconPhone'; +export { IllustrationIconSetting } from './icon/components/IllustrationIconSetting'; +export { IllustrationIconStar } from './icon/components/IllustrationIconStar'; +export { IllustrationIconTag } from './icon/components/IllustrationIconTag'; +export { IllustrationIconTags } from './icon/components/IllustrationIconTags'; +export { IllustrationIconText } from './icon/components/IllustrationIconText'; +export { IllustrationIconToggle } from './icon/components/IllustrationIconToggle'; +export { IllustrationIconUid } from './icon/components/IllustrationIconUid'; +export { IllustrationIconUser } from './icon/components/IllustrationIconUser'; +export { IllustrationIconWrapper } from './icon/components/IllustrationIconWrapper'; +export type { TablerIconsProps } from './icon/components/TablerIcons'; +export { + Icon123, + IconAdjustments, + IconAlertCircle, + IconAlertTriangle, + IconAlignBoxLeftTop, + IconAlignCenter, + IconAlignLeft, + IconAlignRight, + IconApi, + IconApps, + IconAppWindow, + IconArchive, + IconArchiveOff, + IconArrowBackUp, + IconArrowBarToDown, + IconArrowDown, + IconArrowLeft, + IconArrowMerge, + IconArrowRight, + IconArrowsDiagonal, + IconArrowsSort, + IconArrowsSplit2, + IconArrowsVertical, + IconArrowUp, + IconArrowUpRight, + IconAt, + IconAxisX, + IconAxisY, + IconBadge2k, + IconBaselineDensitySmall, + IconBell, + IconBlockquote, + IconBold, + IconBolt, + IconBook, + IconBook2, + IconBookmark, + IconBookmarkPlus, + IconBox, + IconBoxMultiple, + IconBrackets, + IconBracketsAngle, + IconBracketsContain, + IconBrain, + IconBrandDaysCounter, + IconBrandDocker, + IconBrandGithub, + IconBrandGoogle, + IconBrandGraphql, + IconBrandLinkedin, + IconBrandNpm, + IconBrandOpenai, + IconBrandX, + IconBriefcase, + IconBroadcast, + IconBrowserMaximize, + IconBuildingSkyscraper, + IconCalendar, + IconCalendarDue, + IconCalendarEvent, + IconCalendarMonth, + IconCalendarRepeat, + IconCalendarTime, + IconCalendarWeek, + IconCalendarX, + IconCancel, + IconCaretLeft, + IconCaretRight, + IconChartBar, + IconChartBarPopular, + IconChartCandle, + IconChartDots3, + IconChartLine, + IconChartPie, + IconCheck, + IconCheckbox, + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronRightPipe, + IconChevronsRight, + IconChevronUp, + IconCircle, + IconCircleDashed, + IconCircleDot, + IconCircleOff, + IconCirclePlus, + IconCircleX, + IconClick, + IconClock, + IconClockHour8, + IconClockPlay, + IconClockShare, + IconCode, + IconCodeCircle, + IconCoins, + IconColorSwatch, + IconColumnInsertRight, + IconColumns, + IconCommand, + IconComment, + IconCopy, + IconCopyPlus, + IconCpu, + IconCreativeCommonsSa, + IconCreditCard, + IconCsv, + IconCube, + IconCurrencyAfghani, + IconCurrencyBahraini, + IconCurrencyBaht, + IconCurrencyDinar, + IconCurrencyDirham, + IconCurrencyDollar, + IconCurrencyDollarAustralian, + IconCurrencyDollarBrunei, + IconCurrencyDollarCanadian, + IconCurrencyDollarGuyanese, + IconCurrencyDollarSingapore, + IconCurrencyDong, + IconCurrencyDram, + IconCurrencyEuro, + IconCurrencyFlorin, + IconCurrencyForint, + IconCurrencyFrank, + IconCurrencyGuarani, + IconCurrencyHryvnia, + IconCurrencyIranianRial, + IconCurrencyKip, + IconCurrencyKroneCzech, + IconCurrencyKroneDanish, + IconCurrencyKroneSwedish, + IconCurrencyLari, + IconCurrencyLeu, + IconCurrencyLira, + IconCurrencyLyd, + IconCurrencyManat, + IconCurrencyNaira, + IconCurrencyPaanga, + IconCurrencyPeso, + IconCurrencyPound, + IconCurrencyQuetzal, + IconCurrencyReal, + IconCurrencyRenminbi, + IconCurrencyRiyal, + IconCurrencyRubel, + IconCurrencyRufiyaa, + IconCurrencyRupee, + IconCurrencyRupeeNepalese, + IconCurrencyShekel, + IconCurrencySom, + IconCurrencyTaka, + IconCurrencyTenge, + IconCurrencyTugrik, + IconCurrencyWon, + IconCurrencyYen, + IconCurrencyYuan, + IconCurrencyZloty, + IconDatabase, + IconDatabaseExport, + IconDecimal, + IconDeviceFloppy, + IconDoorEnter, + IconDotsVertical, + IconDownload, + IconEdit, + IconEditCircle, + IconEraser, + IconExclamationCircle, + IconExternalLink, + IconEye, + IconEyeOff, + IconEyeShare, + IconEyeX, + IconFile, + IconFileCheck, + IconFileExport, + IconFileImport, + IconFileInfo, + IconFilePencil, + IconFileText, + IconFileUpload, + IconFileZip, + IconFilter, + IconFilterCog, + IconFilterOff, + IconFilterPlus, + IconFilters, + IconFilterX, + IconFlag, + IconFlask, + IconFocusCentered, + IconFolder, + IconFolderOpen, + IconFolderPlus, + IconFolderRoot, + IconFolderSymlink, + IconForbid, + IconFrame, + IconFunction, + IconGauge, + IconGitBranch, + IconGitBranchDeleted, + IconGitCommit, + IconGizmo, + IconGraph, + IconGripVertical, + IconH1, + IconH2, + IconH3, + IconH4, + IconH5, + IconH6, + IconHandClick, + IconHandMove, + IconHeadphones, + IconHeart, + IconHeartOff, + IconHeartRateMonitor, + IconHelp, + IconHelpCircle, + IconHierarchy, + IconHierarchy2, + IconHistory, + IconHistoryToggle, + IconHome, + IconHourglassHigh, + IconHours24, + IconHttpDelete, + IconHttpGet, + IconHttpPatch, + IconHttpPost, + IconHttpPut, + IconId, + IconInbox, + IconInfoCircle, + IconItalic, + IconJetpack, + IconJson, + IconKey, + IconLanguage, + IconLayersLinked, + IconLayout, + IconLayoutDashboard, + IconLayoutGrid, + IconLayoutKanban, + IconLayoutList, + IconLayoutNavbar, + IconLayoutSidebarLeftCollapse, + IconLayoutSidebarLeftExpand, + IconLayoutSidebarRight, + IconLayoutSidebarRightCollapse, + IconLayoutSidebarRightExpand, + IconLego, + IconLetterK, + IconLibraryPlus, + IconLifebuoy, + IconLink, + IconLinkOff, + IconLinkPlus, + IconList, + IconListCheck, + IconListDetails, + IconListNumbers, + IconListSearch, + IconLoader, + IconLock, + IconLockOpen, + IconLogin2, + IconLogout, + IconMail, + IconMailCog, + IconMailX, + IconMap, + IconMathMax, + IconMathMin, + IconMathXy, + IconMaximize, + IconMessage, + IconMessageCirclePlus, + IconMinus, + IconMoneybag, + IconMoodSmile, + IconMoon, + IconMouse2, + IconNewSection, + IconNorthStar, + IconNoteOff, + IconNotes, + IconNumber, + IconNumber123, + IconNumber9, + IconNumbers, + IconPaint, + IconPaperclip, + IconPassword, + IconPencil, + IconPencilOff, + IconPercentage, + IconPhone, + IconPhoto, + IconPhotoUp, + IconPilcrow, + IconPin, + IconPinned, + IconPinnedOff, + IconPlayerPause, + IconPlayerPlay, + IconPlayerStop, + IconPlaylistAdd, + IconPlaystationSquare, + IconPlug, + IconPlus, + IconPoint, + IconPower, + IconPresentation, + IconPrinter, + IconProgressCheck, + IconPrompt, + IconPuzzle, + IconPuzzle2, + IconQuestionMark, + IconRadiusBottomLeft, + IconRadiusBottomRight, + IconRadiusTopLeft, + IconRadiusTopRight, + IconRefresh, + IconRefreshAlert, + IconRefreshDot, + IconRelationManyToMany, + IconRelationOneToMany, + IconRelationOneToOne, + IconReload, + IconReorder, + IconRepeat, + IconRestore, + IconRobot, + IconRocket, + IconRotate, + IconRotate2, + IconRowInsertBottom, + IconRowInsertTop, + IconSearch, + IconSend, + IconServer, + IconServer2, + IconSettings, + IconSettings2, + IconSettingsAutomation, + IconSettingsBolt, + IconShare, + IconShield, + IconSitemap, + IconSlash, + IconSortAscending, + IconSortAscendingLetters, + IconSortAscendingNumbers, + IconSortAZ, + IconSortDescending, + IconSortDescendingLetters, + IconSortDescendingNumbers, + IconSortZA, + IconSparkles, + IconSpy, + IconSql, + IconSquare, + IconSquareCheck, + IconSquareKey, + IconSquareNumber1, + IconSquareNumber2, + IconSquareNumber3, + IconSquareNumber4, + IconSquareNumber5, + IconSquareNumber6, + IconSquareNumber7, + IconSquareNumber8, + IconSquareNumber9, + IconSquarePlus, + IconSquareRoundedCheck, + IconSquareRoundedX, + IconSquareX, + IconStack2, + IconStar, + IconStatusChange, + IconStepInto, + IconStrikethrough, + IconSum, + IconSun, + IconSunMoon, + IconSwitchHorizontal, + IconTable, + IconTag, + IconTags, + IconTarget, + IconTargetArrow, + IconTerminal, + IconTestPipe, + IconTextCaption, + IconTextSize, + IconTextWrap, + IconTimeDuration30, + IconTimeDuration60, + IconTimelineEvent, + IconTool, + IconTransform, + IconTrash, + IconTrashOff, + IconTrashX, + IconTrendingDown, + IconTrendingUp, + IconTypography, + IconUnderline, + IconUnlink, + IconUpload, + IconUser, + IconUserCircle, + IconUserCog, + IconUserPin, + IconUserPlus, + IconUsers, + IconVariable, + IconVariablePlus, + IconVersions, + IconVersionsOff, + IconVideo, + IconWand, + IconWebhook, + IconWindow, + IconWorld, + IconX, +} from './icon/components/TablerIcons'; +export { ThinkingOrbitLoaderIcon } from './icon/components/ThinkingOrbitLoaderIcon'; +export { useIcons } from './icon/hooks/useIcons'; +export { IconsProvider } from './icon/providers/IconsProvider'; +export { iconsState } from './icon/states/iconsState'; +export type { + IconComponentProps, + IconComponent, +} from './icon/types/IconComponent'; +export type { InfoAccent, InfoProps } from './info/components/Info'; +export { Info } from './info/components/Info'; +export { Status } from './status/components/Status'; +export { HorizontalSeparator } from './text/components/HorizontalSeparator'; +export { SeparatorLineText } from './text/components/SeparatorLineText'; +export { StyledTintedIconTileContainer } from './tinted-icon-tile/components/StyledTintedIconTileContainer'; +export type { TintedIconTileProps } from './tinted-icon-tile/components/TintedIconTile'; +export { TintedIconTile } from './tinted-icon-tile/components/TintedIconTile'; +export type { IconTileColorShades } from './tinted-icon-tile/utils/getIconTileColorShades'; +export { getIconTileColorShades } from './tinted-icon-tile/utils/getIconTileColorShades'; +export type { AppTooltipProps } from './tooltip/AppTooltip'; +export { + TooltipPosition, + TooltipDelay, + AppTooltip, +} from './tooltip/AppTooltip'; +export { OverflowingTextWithTooltip } from './tooltip/OverflowingTextWithTooltip'; +export { H1TitleFontColor, H1Title } from './typography/components/H1Title'; +export { H2Title } from './typography/components/H2Title'; +export { H3Title } from './typography/components/H3Title'; +export type { LabelVariant } from './typography/components/Label'; +export { Label } from './typography/components/Label'; +export { + StyledTextContent, + StyledTextWrapper, + StyledText, +} from './typography/components/StyledText'; diff --git a/packages/twenty-ui/src/display/info/components/Info.module.scss b/packages/twenty-ui/src/display/info/components/Info.module.scss new file mode 100644 index 0000000000..52132791a2 --- /dev/null +++ b/packages/twenty-ui/src/display/info/components/Info.module.scss @@ -0,0 +1,38 @@ +.info { + align-items: center; + border-radius: var(--t-border-radius-md); + display: flex; + font-weight: var(--t-font-weight-medium); + justify-content: space-between; + max-width: 512px; + gap: var(--t-spacing-2); + padding: var(--t-spacing-2); + background: transparent; + color: inherit; +} + +.accentBlue { + background: var(--t-color-blue5); + color: var(--t-color-blue10); +} + +.accentDanger { + background: var(--t-color-red3); + color: var(--t-color-red); +} + +.textContainer { + align-items: center; + display: flex; + gap: var(--t-spacing-2); + + & > svg { + flex-shrink: 0; + } +} + +.linkContainer { + & > a { + text-decoration: none; + } +} diff --git a/packages/twenty-ui/src/display/info/components/Info.module.scss.d.ts b/packages/twenty-ui/src/display/info/components/Info.module.scss.d.ts new file mode 100644 index 0000000000..6c8375ba89 --- /dev/null +++ b/packages/twenty-ui/src/display/info/components/Info.module.scss.d.ts @@ -0,0 +1,8 @@ +declare const classNames: { + readonly info: 'info'; + readonly accentBlue: 'accentBlue'; + readonly accentDanger: 'accentDanger'; + readonly textContainer: 'textContainer'; + readonly linkContainer: 'linkContainer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/info/components/Info.tsx b/packages/twenty-ui/src/display/info/components/Info.tsx new file mode 100644 index 0000000000..f3b04c6967 --- /dev/null +++ b/packages/twenty-ui/src/display/info/components/Info.tsx @@ -0,0 +1,63 @@ +import { clsx } from 'clsx'; +import { IconInfoCircle } from '@ui/display/icon/components/TablerIcons'; +import { ThemeContext } from '@ui/theme-constants'; + +import { Button } from '@ui/input/button/components/Button/Button'; +import React, { useContext } from 'react'; +import { Link } from 'react-router-dom'; + +import styles from './Info.module.scss'; + +export type InfoAccent = 'blue' | 'danger'; +export type InfoProps = { + accent?: InfoAccent; + text: string; + buttonTitle?: string; + onClick?: (event: React.MouseEvent) => void; + to?: string; +}; + +const INFO_ACCENT_CLASS_NAMES: Record = { + blue: styles.accentBlue, + danger: styles.accentDanger, +}; + +export const Info = ({ + accent = 'blue', + text, + buttonTitle, + onClick, + to, +}: InfoProps) => { + const { theme } = useContext(ThemeContext); + + return ( +
+
+ + {text} +
+ {buttonTitle && to && ( + + +
+ ); +}; diff --git a/packages/twenty-ui/src/display/info/components/__stories__/Info.stories.tsx b/packages/twenty-ui/src/display/info/components/__stories__/Info.stories.tsx new file mode 100644 index 0000000000..3b4a8c39c0 --- /dev/null +++ b/packages/twenty-ui/src/display/info/components/__stories__/Info.stories.tsx @@ -0,0 +1,51 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { + CatalogDecorator, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; + +import { Info, type InfoAccent } from '../Info'; + +const meta: Meta = { + title: 'UI/Display/Info', + component: Info, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + accent: 'blue', + text: 'An info component', + buttonTitle: 'Update', + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: { + text: 'An info component', + buttonTitle: 'Update', + }, + argTypes: { + accent: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + catalog: { + dimensions: [ + { + name: 'accents', + values: ['blue', 'danger'] satisfies InfoAccent[], + props: (accent: InfoAccent) => ({ accent }), + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/display/status/components/Status.module.scss b/packages/twenty-ui/src/display/status/components/Status.module.scss new file mode 100644 index 0000000000..b7c65ea5c6 --- /dev/null +++ b/packages/twenty-ui/src/display/status/components/Status.module.scss @@ -0,0 +1,42 @@ +.status { + align-items: center; + background: var(--status-background); + border-radius: var(--t-border-radius-pill); + color: var(--status-text-color); + display: inline-flex; + font-size: var(--t-font-size-md); + font-style: normal; + gap: var(--t-spacing-1); + height: var(--t-spacing-5); + margin: 0; + overflow: hidden; + padding: 0 var(--t-spacing-2) 0 var(--t-spacing-2); + + &[data-loader-visible] { + padding-right: var(--t-spacing-1); + } + + &:before { + background-color: var(--status-text-color); + border-radius: var(--t-border-radius-rounded); + content: ''; + display: block; + flex-shrink: 0; + height: var(--t-spacing-1); + width: var(--t-spacing-1); + } +} + +.regular { + font-weight: var(--t-font-weight-regular); +} + +.medium { + font-weight: var(--t-font-weight-medium); +} + +.content { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/packages/twenty-ui/src/display/status/components/Status.module.scss.d.ts b/packages/twenty-ui/src/display/status/components/Status.module.scss.d.ts new file mode 100644 index 0000000000..0b339436dc --- /dev/null +++ b/packages/twenty-ui/src/display/status/components/Status.module.scss.d.ts @@ -0,0 +1,7 @@ +declare const classNames: { + readonly status: 'status'; + readonly regular: 'regular'; + readonly medium: 'medium'; + readonly content: 'content'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/status/components/Status.tsx b/packages/twenty-ui/src/display/status/components/Status.tsx new file mode 100644 index 0000000000..b667477de6 --- /dev/null +++ b/packages/twenty-ui/src/display/status/components/Status.tsx @@ -0,0 +1,45 @@ +import { clsx } from 'clsx'; + +import { Loader } from '@ui/feedback/loader/components/Loader'; +import { type ThemeColor } from '@ui/theme'; +import { themeCssVariables } from '@ui/theme-constants'; +import { parseThemeColor } from '@ui/utilities'; + +import styles from './Status.module.scss'; + +type StatusProps = { + className?: string; + color: ThemeColor; + isLoaderVisible?: boolean; + text: string; + onClick?: () => void; + weight?: 'regular' | 'medium'; +}; + +export const Status = ({ + className, + color, + isLoaderVisible = false, + text, + onClick, + weight = 'regular', +}: StatusProps) => { + const parsedColor = parseThemeColor(color); + + return ( +

+ {text} + {isLoaderVisible ? : null} +

+ ); +}; diff --git a/packages/twenty-ui/src/display/status/components/__stories__/Status.stories.tsx b/packages/twenty-ui/src/display/status/components/__stories__/Status.stories.tsx new file mode 100644 index 0000000000..fa785b3e76 --- /dev/null +++ b/packages/twenty-ui/src/display/status/components/__stories__/Status.stories.tsx @@ -0,0 +1,71 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { + CatalogDecorator, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; +import { type ThemeColor, MAIN_COLOR_NAMES } from '@ui/theme'; +import { expect, fn, userEvent, within } from 'storybook/test'; + +import { Status } from '../Status'; + +const meta: Meta = { + title: 'UI/Display/Status/Status', + component: Status, + args: { + text: 'Urgent', + weight: 'medium', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + color: 'red', + onClick: fn(), + }, + decorators: [ComponentDecorator], + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const status = canvas.getByRole('heading', { level: 3 }); + + await userEvent.click(status); + expect(args.onClick).toHaveBeenCalled(); + }, +}; + +export const WithLongText: Story = { + decorators: [ComponentDecorator], + args: { + color: 'green', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + container: { width: 100 }, + }, +}; + +export const Catalog: CatalogStory = { + argTypes: { + color: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + catalog: { + dimensions: [ + { + name: 'colors', + values: MAIN_COLOR_NAMES, + props: (color: ThemeColor) => ({ color }), + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/display/text/components/HorizontalSeparator.module.scss b/packages/twenty-ui/src/display/text/components/HorizontalSeparator.module.scss new file mode 100644 index 0000000000..8a92cb96b0 --- /dev/null +++ b/packages/twenty-ui/src/display/text/components/HorizontalSeparator.module.scss @@ -0,0 +1,50 @@ +.separator { + background-color: var( + --horizontal-separator-color, + var(--t-border-color-medium) + ); + height: 0; + flex-shrink: 0; + margin-bottom: var(--t-spacing-3); + margin-top: var(--t-spacing-3); + width: 100%; + + &[data-visible] { + height: 1px; + } + + &[data-no-margin] { + margin-bottom: 0; + margin-top: 0; + } +} + +.separatorContainer { + align-items: center; + display: flex; + margin-bottom: var(--t-spacing-3); + margin-top: var(--t-spacing-3); + width: 100%; + + &[data-no-margin] { + margin-bottom: 0; + margin-top: 0; + } +} + +.line { + background-color: var( + --horizontal-separator-color, + var(--t-border-color-medium) + ); + height: 0; + flex-grow: 1; + + &[data-visible] { + height: 1px; + } +} + +.text { + margin: 0 var(--t-spacing-2); +} diff --git a/packages/twenty-ui/src/display/text/components/HorizontalSeparator.module.scss.d.ts b/packages/twenty-ui/src/display/text/components/HorizontalSeparator.module.scss.d.ts new file mode 100644 index 0000000000..6f718471ec --- /dev/null +++ b/packages/twenty-ui/src/display/text/components/HorizontalSeparator.module.scss.d.ts @@ -0,0 +1,7 @@ +declare const classNames: { + readonly separator: 'separator'; + readonly separatorContainer: 'separatorContainer'; + readonly line: 'line'; + readonly text: 'text'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/text/components/HorizontalSeparator.tsx b/packages/twenty-ui/src/display/text/components/HorizontalSeparator.tsx new file mode 100644 index 0000000000..bc47161ae9 --- /dev/null +++ b/packages/twenty-ui/src/display/text/components/HorizontalSeparator.tsx @@ -0,0 +1,48 @@ +import { type JSX } from 'react'; + +import { Label } from '@ui/display'; + +import styles from './HorizontalSeparator.module.scss'; + +type HorizontalSeparatorProps = { + visible?: boolean; + text?: string; + noMargin?: boolean; + color?: string; +}; + +export const HorizontalSeparator = ({ + visible = true, + text = '', + noMargin = false, + color, +}: HorizontalSeparatorProps): JSX.Element => { + const colorStyle = color + ? ({ '--horizontal-separator-color': color } as React.CSSProperties) + : undefined; + + return ( + <> + {text ? ( +
+
+ +
+
+ ) : ( +
+ )} + + ); +}; diff --git a/packages/twenty-ui/src/display/text/components/SeparatorLineText.module.scss b/packages/twenty-ui/src/display/text/components/SeparatorLineText.module.scss new file mode 100644 index 0000000000..e2537443f7 --- /dev/null +++ b/packages/twenty-ui/src/display/text/components/SeparatorLineText.module.scss @@ -0,0 +1,24 @@ +.container { + display: flex; + align-items: center; + width: 100%; + font-size: var(--t-font-size-md); + font-weight: var(--t-font-weight-semi-bold); + color: var(--t-font-color-extra-light); + + &::before, + &::after { + content: ''; + height: 1px; + flex-grow: 1; + background: var(--t-background-transparent-light); + } + + &::before { + margin: 0 var(--t-spacing-4) 0 0; + } + + &::after { + margin: 0 0 0 var(--t-spacing-4); + } +} diff --git a/packages/twenty-ui/src/display/text/components/SeparatorLineText.module.scss.d.ts b/packages/twenty-ui/src/display/text/components/SeparatorLineText.module.scss.d.ts new file mode 100644 index 0000000000..8df71b4dea --- /dev/null +++ b/packages/twenty-ui/src/display/text/components/SeparatorLineText.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly container: 'container'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/text/components/SeparatorLineText.tsx b/packages/twenty-ui/src/display/text/components/SeparatorLineText.tsx new file mode 100644 index 0000000000..46f0d64f27 --- /dev/null +++ b/packages/twenty-ui/src/display/text/components/SeparatorLineText.tsx @@ -0,0 +1,9 @@ +import styles from './SeparatorLineText.module.scss'; + +export const SeparatorLineText = ({ + children, +}: { + children: React.ReactNode; +}) => { + return
{children}
; +}; diff --git a/packages/twenty-ui/src/display/text/components/__stories__/HorizontalSeparator.stories.tsx b/packages/twenty-ui/src/display/text/components/__stories__/HorizontalSeparator.stories.tsx new file mode 100644 index 0000000000..2b72597e1c --- /dev/null +++ b/packages/twenty-ui/src/display/text/components/__stories__/HorizontalSeparator.stories.tsx @@ -0,0 +1,15 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { ComponentDecorator } from '@ui/testing'; + +import { HorizontalSeparator } from '../HorizontalSeparator'; + +const meta: Meta = { + title: 'UI/Display/Text/HorizontalSeparator', + component: HorizontalSeparator, + decorators: [ComponentDecorator], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/packages/twenty-ui/src/display/text/components/__stories__/SeparatorLineText.stories.tsx b/packages/twenty-ui/src/display/text/components/__stories__/SeparatorLineText.stories.tsx new file mode 100644 index 0000000000..44957bf5ac --- /dev/null +++ b/packages/twenty-ui/src/display/text/components/__stories__/SeparatorLineText.stories.tsx @@ -0,0 +1,19 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { ComponentDecorator } from '@ui/testing'; + +import { SeparatorLineText } from '../SeparatorLineText'; + +const meta: Meta = { + title: 'UI/Display/Text/SeparatorLineText', + component: SeparatorLineText, + args: { children: 'Or' }, + decorators: [ComponentDecorator], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, +}; diff --git a/packages/twenty-ui/src/display/tinted-icon-tile/components/StyledTintedIconTileContainer.module.scss b/packages/twenty-ui/src/display/tinted-icon-tile/components/StyledTintedIconTileContainer.module.scss new file mode 100644 index 0000000000..2925496a7b --- /dev/null +++ b/packages/twenty-ui/src/display/tinted-icon-tile/components/StyledTintedIconTileContainer.module.scss @@ -0,0 +1,12 @@ +.root { + box-sizing: border-box; + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: var(--tinted-icon-tile-dimension, var(--t-spacing-4)); + height: var(--tinted-icon-tile-dimension, var(--t-spacing-4)); + border: var(--tinted-icon-tile-border, none); + border-radius: 4px; + background-color: var(--tinted-icon-tile-background-color, transparent); +} diff --git a/packages/twenty-ui/src/display/tinted-icon-tile/components/StyledTintedIconTileContainer.module.scss.d.ts b/packages/twenty-ui/src/display/tinted-icon-tile/components/StyledTintedIconTileContainer.module.scss.d.ts new file mode 100644 index 0000000000..4d6743c3dc --- /dev/null +++ b/packages/twenty-ui/src/display/tinted-icon-tile/components/StyledTintedIconTileContainer.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly root: 'root'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/tinted-icon-tile/components/StyledTintedIconTileContainer.tsx b/packages/twenty-ui/src/display/tinted-icon-tile/components/StyledTintedIconTileContainer.tsx new file mode 100644 index 0000000000..f10aeb0afa --- /dev/null +++ b/packages/twenty-ui/src/display/tinted-icon-tile/components/StyledTintedIconTileContainer.tsx @@ -0,0 +1,35 @@ +import { clsx } from 'clsx'; + +import styles from './StyledTintedIconTileContainer.module.scss'; + +type StyledTintedIconTileContainerProps = + React.ComponentPropsWithoutRef<'div'> & { + $backgroundColor?: string; + $borderColor?: string; + $dimension?: string; + }; + +export const StyledTintedIconTileContainer = ({ + $backgroundColor, + $borderColor, + $dimension, + className, + style, + ...rest +}: StyledTintedIconTileContainerProps) => ( +
+); diff --git a/packages/twenty-ui/src/display/tinted-icon-tile/components/TintedIconTile.tsx b/packages/twenty-ui/src/display/tinted-icon-tile/components/TintedIconTile.tsx new file mode 100644 index 0000000000..d0f4a45161 --- /dev/null +++ b/packages/twenty-ui/src/display/tinted-icon-tile/components/TintedIconTile.tsx @@ -0,0 +1,39 @@ +import { type IconComponent } from '@ui/display/icon/types/IconComponent'; +import { StyledTintedIconTileContainer } from '@ui/display/tinted-icon-tile/components/StyledTintedIconTileContainer'; +import { getIconTileColorShades } from '@ui/display/tinted-icon-tile/utils/getIconTileColorShades'; +import { DEFAULT_THEME_COLOR_FALLBACK } from '@ui/theme'; +import { ThemeContext } from '@ui/theme-constants'; +import { useContext } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +export type TintedIconTileProps = { + Icon: IconComponent; + color?: string | null; + size?: number; + stroke?: number; +}; + +export const TintedIconTile = ({ + Icon, + color = DEFAULT_THEME_COLOR_FALLBACK, + size: sizeFromProps, + stroke: strokeFromProps, +}: TintedIconTileProps) => { + const { theme } = useContext(ThemeContext); + const style = getIconTileColorShades(color); + const iconSize = sizeFromProps ?? theme.icon.size.md; + const iconStroke = strokeFromProps ?? theme.icon.stroke.md; + const tileDimension = isDefined(sizeFromProps) + ? `${sizeFromProps}px` + : undefined; + + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/tinted-icon-tile/internal/constants/tintedIconTileColorShades.constant.ts b/packages/twenty-ui/src/display/tinted-icon-tile/internal/constants/tintedIconTileColorShades.constant.ts new file mode 100644 index 0000000000..3d2f44f3a6 --- /dev/null +++ b/packages/twenty-ui/src/display/tinted-icon-tile/internal/constants/tintedIconTileColorShades.constant.ts @@ -0,0 +1,5 @@ +export const TINTED_ICON_TILE_COLOR_SHADES = { + background: 5, + border: 6, + icon: 11, +} as const; diff --git a/packages/twenty-ui/src/display/tinted-icon-tile/internal/utils/getColorFromTheme.ts b/packages/twenty-ui/src/display/tinted-icon-tile/internal/utils/getColorFromTheme.ts new file mode 100644 index 0000000000..6325f91f15 --- /dev/null +++ b/packages/twenty-ui/src/display/tinted-icon-tile/internal/utils/getColorFromTheme.ts @@ -0,0 +1,15 @@ +import { type ThemeColor } from '@ui/theme'; +import { themeCssVariables } from '@ui/theme-constants'; + +export const getColorFromTheme = ( + themeColor: ThemeColor, + shade: number, +): string => { + const colorMap = themeCssVariables.color as unknown as Record; + const tagText = themeCssVariables.tag.text as unknown as Record< + string, + string + >; + const key = `${themeColor}${shade}`; + return colorMap[key] ?? tagText[themeColor]; +}; diff --git a/packages/twenty-ui/src/display/tinted-icon-tile/utils/getIconTileColorShades.ts b/packages/twenty-ui/src/display/tinted-icon-tile/utils/getIconTileColorShades.ts new file mode 100644 index 0000000000..0c225e87a6 --- /dev/null +++ b/packages/twenty-ui/src/display/tinted-icon-tile/utils/getIconTileColorShades.ts @@ -0,0 +1,29 @@ +import { TINTED_ICON_TILE_COLOR_SHADES } from '@ui/display/tinted-icon-tile/internal/constants/tintedIconTileColorShades.constant'; +import { getColorFromTheme } from '@ui/display/tinted-icon-tile/internal/utils/getColorFromTheme'; +import { parseThemeColor } from '@ui/utilities'; + +export type IconTileColorShades = { + backgroundColor: string; + iconColor: string; + borderColor: string; +}; + +export const getIconTileColorShades = ( + color: string | null | undefined, +): IconTileColorShades => { + const themeColor = parseThemeColor(color); + return { + backgroundColor: getColorFromTheme( + themeColor, + TINTED_ICON_TILE_COLOR_SHADES.background, + ), + iconColor: getColorFromTheme( + themeColor, + TINTED_ICON_TILE_COLOR_SHADES.icon, + ), + borderColor: getColorFromTheme( + themeColor, + TINTED_ICON_TILE_COLOR_SHADES.border, + ), + }; +}; diff --git a/packages/twenty-ui/src/display/tooltip/AppTooltip.module.scss b/packages/twenty-ui/src/display/tooltip/AppTooltip.module.scss new file mode 100644 index 0000000000..03079cd6ca --- /dev/null +++ b/packages/twenty-ui/src/display/tooltip/AppTooltip.module.scss @@ -0,0 +1,62 @@ +.positioner { + // the tooltip never intercepts pointer events unless clickable, + // like react-tooltip's core styles + pointer-events: none; + z-index: var(--t-last-layer-z-index); +} + +.tooltip { + backdrop-filter: var(--t-blur-strong); + background-color: var(--t-color-transparent-gray11); + border-radius: var(--t-border-radius-sm); + box-shadow: var(--t-box-shadow-light); + color: var(--t-gray-scale-gray1); + font-size: var(--t-font-size-sm); + font-weight: var(--t-font-weight-regular); + overflow: visible; + padding: var(--t-spacing-2); + word-break: break-word; + width: max-content; + max-width: 100%; + // react-tooltip showed tooltips at 0.9 opacity with 0.15s fades + opacity: 0.9; + will-change: opacity; + transition: opacity 0.15s ease-out; + + &[data-starting-style], + &[data-ending-style] { + opacity: 0; + } + + &[data-ending-style] { + transition-timing-function: ease-in; + } +} + +.clickable { + pointer-events: auto; +} + +.arrow { + position: absolute; + width: 8px; + height: 8px; + background: inherit; + transform: rotate(45deg); + + &[data-side='top'] { + bottom: -4px; + } + + &[data-side='bottom'] { + top: -4px; + } + + &[data-side='left'] { + right: -4px; + } + + &[data-side='right'] { + left: -4px; + } +} diff --git a/packages/twenty-ui/src/display/tooltip/AppTooltip.module.scss.d.ts b/packages/twenty-ui/src/display/tooltip/AppTooltip.module.scss.d.ts new file mode 100644 index 0000000000..188739cb85 --- /dev/null +++ b/packages/twenty-ui/src/display/tooltip/AppTooltip.module.scss.d.ts @@ -0,0 +1,7 @@ +declare const classNames: { + readonly positioner: 'positioner'; + readonly tooltip: 'tooltip'; + readonly clickable: 'clickable'; + readonly arrow: 'arrow'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/tooltip/AppTooltip.tsx b/packages/twenty-ui/src/display/tooltip/AppTooltip.tsx new file mode 100644 index 0000000000..492892148b --- /dev/null +++ b/packages/twenty-ui/src/display/tooltip/AppTooltip.tsx @@ -0,0 +1,312 @@ +import { Tooltip } from '@base-ui/react/tooltip'; +import { isNonEmptyString } from '@sniptt/guards'; +import { clsx } from 'clsx'; +import { useEffect, useRef, useState } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +import styles from './AppTooltip.module.scss'; + +export enum TooltipPosition { + Top = 'top', + Left = 'left', + Right = 'right', + Bottom = 'bottom', +} + +export enum TooltipDelay { + noDelay = '0ms', + shortDelay = '300ms', + mediumDelay = '500ms', + longDelay = '1000ms', +} + +// Same shapes as react-tooltip's PlacesType and PositionStrategy, kept for +// public API parity with the deprecated AppTooltip +type PlacesType = + | 'top' + | 'top-start' + | 'top-end' + | 'right' + | 'right-start' + | 'right-end' + | 'bottom' + | 'bottom-start' + | 'bottom-end' + | 'left' + | 'left-start' + | 'left-end'; + +type PositionStrategy = 'absolute' | 'fixed'; + +// react-tooltip's delayHide that the deprecated AppTooltip hardcoded +const HIDE_DELAY_MS = 20; + +// react-tooltip's default anchor offset +const DEFAULT_OFFSET = 10; + +export type AppTooltipProps = { + className?: string; + anchorSelect?: string; + content?: string; + children?: React.ReactNode; + offset?: number; + noArrow?: boolean; + hidden?: boolean; + place?: PlacesType; + delay?: TooltipDelay; + positionStrategy?: PositionStrategy; + clickable?: boolean; + width?: string; + isOpen?: boolean; +}; + +export const AppTooltip = ({ + anchorSelect, + className, + content, + hidden = false, + noArrow, + offset, + delay = TooltipDelay.mediumDelay, + place, + positionStrategy, + children, + clickable, + width, + isOpen, +}: AppTooltipProps) => { + const getDelayInMis = (delay: TooltipDelay) => { + switch (delay) { + case TooltipDelay.noDelay: + return 0; + case TooltipDelay.shortDelay: + return 300; + case TooltipDelay.mediumDelay: + return 500; + case TooltipDelay.longDelay: + return 1000; + } + }; + + const [show, setShow] = useState(false); + const [anchorElements, setAnchorElements] = useState([]); + const [activeAnchor, setActiveAnchor] = useState(null); + + const showDelayTimerRef = useRef | undefined>( + undefined, + ); + const hideDelayTimerRef = useRef | undefined>( + undefined, + ); + // Mirrors react-tooltip's `rendered` flag: while the tooltip is still + // mounted (including its fade-out), re-entering an anchor reopens it + // without waiting for the show delay again + const isRenderedRef = useRef(false); + const isHoveringTooltipRef = useRef(false); + + const isControlled = isDefined(isOpen); + const delayShowMs = getDelayInMis(delay); + + // Track anchors matching the selector, including ones added to the DOM + // later, like react-tooltip does with its MutationObserver + useEffect(() => { + if (!isNonEmptyString(anchorSelect)) { + setAnchorElements([]); + return; + } + + const queryAnchorElements = () => { + try { + return Array.from(document.querySelectorAll(anchorSelect)); + } catch { + return []; + } + }; + + setAnchorElements(queryAnchorElements()); + + const observer = new MutationObserver(() => { + setAnchorElements((previousAnchorElements) => { + const newAnchorElements = queryAnchorElements(); + + const hasSameAnchors = + newAnchorElements.length === previousAnchorElements.length && + newAnchorElements.every( + (anchorElement, index) => + anchorElement === previousAnchorElements[index], + ); + + return hasSameAnchors ? previousAnchorElements : newAnchorElements; + }); + }); + + // also watch data-tooltip-id like react-tooltip, since anchors are + // commonly matched with a [data-tooltip-id='...'] selector + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['data-tooltip-id'], + }); + + return () => observer.disconnect(); + }, [anchorSelect]); + + // Like react-tooltip, fall back to the first matched anchor so that a + // controlled tooltip (isOpen) can position itself without a hover event + useEffect(() => { + if (isDefined(activeAnchor) && anchorElements.includes(activeAnchor)) { + return; + } + + setActiveAnchor(anchorElements[0] ?? null); + }, [anchorElements, activeAnchor]); + + useEffect(() => { + if (anchorElements.length === 0) { + return; + } + + const handleShow = (value: boolean) => { + if (!isControlled) { + setShow(value); + } + }; + + const handleAnchorEnter = (anchorElement: Element) => { + if (!anchorElement.isConnected) { + setActiveAnchor(null); + return; + } + + if (delayShowMs > 0) { + clearTimeout(showDelayTimerRef.current); + if (isRenderedRef.current) { + // the tooltip is already visible, ignore the show delay + handleShow(true); + } else { + showDelayTimerRef.current = setTimeout(() => { + handleShow(true); + }, delayShowMs); + } + } else { + handleShow(true); + } + + setActiveAnchor(anchorElement); + clearTimeout(hideDelayTimerRef.current); + }; + + const handleAnchorLeave = () => { + clearTimeout(hideDelayTimerRef.current); + hideDelayTimerRef.current = setTimeout(() => { + if (isHoveringTooltipRef.current) { + return; + } + handleShow(false); + }, HIDE_DELAY_MS); + clearTimeout(showDelayTimerRef.current); + }; + + const removeListeners = anchorElements.map((anchorElement) => { + const handleEnter = () => handleAnchorEnter(anchorElement); + const handleLeave = () => handleAnchorLeave(); + + // mouseover/mouseout instead of mouseenter/mouseleave to replicate + // react-tooltip's default open and close events + anchorElement.addEventListener('mouseover', handleEnter); + anchorElement.addEventListener('mouseout', handleLeave); + anchorElement.addEventListener('focus', handleEnter); + anchorElement.addEventListener('blur', handleLeave); + + return () => { + anchorElement.removeEventListener('mouseover', handleEnter); + anchorElement.removeEventListener('mouseout', handleLeave); + anchorElement.removeEventListener('focus', handleEnter); + anchorElement.removeEventListener('blur', handleLeave); + }; + }); + + return () => removeListeners.forEach((removeListener) => removeListener()); + }, [anchorElements, isControlled, delayShowMs]); + + useEffect(() => { + return () => { + clearTimeout(showDelayTimerRef.current); + clearTimeout(hideDelayTimerRef.current); + }; + }, []); + + const handleTooltipMouseEnter = () => { + isHoveringTooltipRef.current = true; + }; + + const handleTooltipMouseLeave = () => { + isHoveringTooltipRef.current = false; + clearTimeout(hideDelayTimerRef.current); + hideDelayTimerRef.current = setTimeout(() => { + if (isHoveringTooltipRef.current) { + return; + } + if (!isControlled) { + setShow(false); + } + }, HIDE_DELAY_MS); + clearTimeout(showDelayTimerRef.current); + }; + + // react-tooltip's content priority: content prop wins over children + const renderedContent = isNonEmptyString(content) ? content : children; + + const isTooltipOpen = + !hidden && + (isOpen ?? show) && + Boolean(renderedContent) && + isDefined(activeAnchor); + + useEffect(() => { + if (isTooltipOpen) { + isRenderedRef.current = true; + } + }, [isTooltipOpen]); + + const [parsedSide, parsedAlign] = (place ?? 'top').split('-'); + const side = parsedSide as 'top' | 'right' | 'bottom' | 'left'; + const align = (parsedAlign ?? 'center') as 'start' | 'center' | 'end'; + + return ( + { + if (!openValue) { + isRenderedRef.current = false; + } + }} + > + + + + {renderedContent} + {!noArrow && } + + + + + ); +}; diff --git a/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.module.scss b/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.module.scss new file mode 100644 index 0000000000..7c065fb3ee --- /dev/null +++ b/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.module.scss @@ -0,0 +1,46 @@ +.overflowingMultilineText { + cursor: inherit; + font-family: inherit; + font-size: inherit; + font-weight: inherit; + max-width: 100%; + overflow: hidden; + text-decoration: inherit; + text-overflow: ellipsis; + height: auto; + -webkit-line-clamp: var(--displayed-max-rows, 1); + display: -webkit-box; + -webkit-box-orient: vertical; + white-space: pre-wrap; + overflow-wrap: break-word; + + &[data-content-overflowing] { + cursor: pointer; + } +} + +.overflowingText { + cursor: inherit; + font-family: inherit; + font-size: inherit; + font-weight: inherit; + max-width: 100%; + text-decoration: inherit; + text-overflow: ellipsis; + overflow: hidden; + height: auto; + white-space: nowrap; + + &[data-content-overflowing] { + cursor: pointer; + } +} + +.large { + height: var(--t-spacing-4); +} + +.pre { + font-family: inherit; + white-space: pre-wrap; +} diff --git a/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.module.scss.d.ts b/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.module.scss.d.ts new file mode 100644 index 0000000000..79976f24d0 --- /dev/null +++ b/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.module.scss.d.ts @@ -0,0 +1,7 @@ +declare const classNames: { + readonly overflowingMultilineText: 'overflowingMultilineText'; + readonly overflowingText: 'overflowingText'; + readonly large: 'large'; + readonly pre: 'pre'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx b/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx new file mode 100644 index 0000000000..bc28c117a6 --- /dev/null +++ b/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx @@ -0,0 +1,137 @@ +import { type CSSProperties, type ReactNode, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; + +import { isNonEmptyString } from '@sniptt/guards'; +import { clsx } from 'clsx'; +import { LinkifiedText } from '@ui/display/components/LinkifiedText'; +import { isDefined } from '@ui/utilities/utils/isDefined'; +import { AppTooltip, TooltipDelay } from './AppTooltip'; + +import styles from './OverflowingTextWithTooltip.module.scss'; + +type OverflowingTextWithTooltipProps = { + size?: 'large' | 'small'; + isTooltipMultiline?: boolean; + displayedMaxRows?: number; + tooltipDelay?: TooltipDelay; + alwaysShowTooltip?: boolean; +} & ( + | { + text: string | null | undefined; + tooltipContent?: string; + } + | { + text: Exclude; + tooltipContent: string; + } +); + +export const OverflowingTextWithTooltip = ({ + size = 'small', + text, + isTooltipMultiline, + displayedMaxRows, + tooltipContent, + tooltipDelay = TooltipDelay.mediumDelay, + alwaysShowTooltip = false, +}: OverflowingTextWithTooltipProps) => { + const textElementId = `title-id-${+new Date()}`; + + const textRef = useRef(null); + + const [isTitleOverflowing, setIsTitleOverflowing] = useState(false); + const [shouldRenderTooltip, setShouldRenderTooltip] = useState(false); + + const handleMouseEnter = () => { + const isOverflowing = textRef.current + ? textRef.current?.scrollHeight > textRef.current?.clientHeight || + textRef.current.scrollWidth > textRef.current.clientWidth + : false; + + setIsTitleOverflowing(isOverflowing); + setShouldRenderTooltip(true); + }; + + const handleMouseLeave = () => { + setIsTitleOverflowing(false); + setShouldRenderTooltip(false); + }; + + const handleTooltipClick = (event: React.MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + }; + + const tooltipText = isNonEmptyString(tooltipContent) + ? tooltipContent + : isNonEmptyString(text) + ? text + : null; + + return ( + <> + {isDefined(displayedMaxRows) ? ( +
+ {isNonEmptyString(text) ? : text} +
+ ) : ( +
+ {isNonEmptyString(text) ? : text} +
+ )} + + {shouldRenderTooltip && + (isTitleOverflowing || alwaysShowTooltip) && + isDefined(tooltipText) && + createPortal( +
+ + {isTooltipMultiline ? ( +
{tooltipText}
+ ) : ( + tooltipText + )} +
+
, + document.body, + )} + + ); +}; diff --git a/packages/twenty-ui/src/display/tooltip/__stories__/OverflowTextWithTooltip.stories.tsx b/packages/twenty-ui/src/display/tooltip/__stories__/OverflowTextWithTooltip.stories.tsx new file mode 100644 index 0000000000..4f66dbf251 --- /dev/null +++ b/packages/twenty-ui/src/display/tooltip/__stories__/OverflowTextWithTooltip.stories.tsx @@ -0,0 +1,137 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { userEvent, within } from 'storybook/test'; + +import { ComponentDecorator } from '@ui/testing'; + +import { OverflowingTextWithTooltip } from '../OverflowingTextWithTooltip'; + +const longText = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi tellus diam, rhoncus nec consequat quis, dapibus quis massa. Praesent tincidunt augue at ex bibendum, non finibus augue faucibus. In at gravida orci. Nulla facilisi. Proin ut augue ut nisi pellentesque tristique. Proin sodales libero id turpis tincidunt posuere. Sed euismod, nunc at aliquam ultricies, nunc nisl aliquet nunc, quis aliquam nisl nunc quis nunc. Donec euismod, nunc quis aliquam ultricies, nunc nisl aliquet nunc.'; + +const meta: Meta = { + title: 'UI/Display/Tooltip/OverflowingTextWithTooltip', + component: OverflowingTextWithTooltip, +}; + +export default meta; +type Story = StoryObj; + +export const SingleLineOverflowing: Story = { + args: { + text: longText, + }, + decorators: [ComponentDecorator], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const tooltip = await canvas.findByTestId('tooltip'); + await userEvent.hover(tooltip); + }, +}; + +export const SingleLineNotOverflowing: Story = { + args: { + text: 'Short', + }, + decorators: [ComponentDecorator], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const tooltip = await canvas.findByTestId('tooltip'); + await userEvent.hover(tooltip); + }, +}; + +export const MultilineOverflowing: Story = { + args: { + text: longText, + displayedMaxRows: 2, + }, + decorators: [ComponentDecorator], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const tooltip = await canvas.findByTestId('tooltip'); + await userEvent.hover(tooltip); + }, +}; + +export const MultilineNotOverflowing: Story = { + args: { + text: 'Short', + displayedMaxRows: 2, + }, + decorators: [ComponentDecorator], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const tooltip = await canvas.findByTestId('tooltip'); + await userEvent.hover(tooltip); + }, +}; + +export const SingleLineWithReactNodeOverflowing: Story = { + args: { + text: ( + <> + {longText} · Secondary Label + + ), + tooltipContent: `${longText} · Secondary Label`, + }, + decorators: [ComponentDecorator], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const tooltip = await canvas.findByTestId('tooltip'); + await userEvent.hover(tooltip); + }, +}; + +export const SingleLineWithReactNodeNotOverflowing: Story = { + args: { + text: ( + <> + A · B + + ), + tooltipContent: 'A · B', + }, + decorators: [ComponentDecorator], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const tooltip = await canvas.findByTestId('tooltip'); + await userEvent.hover(tooltip); + }, +}; + +export const MultilineWithReactNodeOverflowing: Story = { + args: { + text: ( + <> + Hello! {longText} · Important Note + + ), + tooltipContent: `Hello! ${longText} · Important Note`, + displayedMaxRows: 2, + }, + decorators: [ComponentDecorator], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const tooltip = await canvas.findByTestId('tooltip'); + await userEvent.hover(tooltip); + }, +}; + +export const MultilineWithReactNodeNotOverflowing: Story = { + args: { + text: ( + <> + A · B + + ), + tooltipContent: 'A · B', + displayedMaxRows: 2, + }, + decorators: [ComponentDecorator], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const tooltip = await canvas.findByTestId('tooltip'); + await userEvent.hover(tooltip); + }, +}; diff --git a/packages/twenty-ui/src/display/tooltip/__stories__/Tooltip.stories.tsx b/packages/twenty-ui/src/display/tooltip/__stories__/Tooltip.stories.tsx new file mode 100644 index 0000000000..ba1d73d435 --- /dev/null +++ b/packages/twenty-ui/src/display/tooltip/__stories__/Tooltip.stories.tsx @@ -0,0 +1,174 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { + CatalogDecorator, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; + +import { + AppTooltip as Tooltip, + TooltipDelay, + TooltipPosition, +} from '../AppTooltip'; + +const meta: Meta = { + title: 'UI/Display/Tooltip', + component: Tooltip, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + place: TooltipPosition.Bottom, + delay: TooltipDelay.mediumDelay, + content: 'Tooltip Test', + hidden: false, + anchorSelect: '#hover-text', + }, + decorators: [ComponentDecorator], + render: ({ + anchorSelect, + className, + content, + delay, + hidden, + noArrow, + offset, + place, + positionStrategy, + clickable, + }) => ( + <> +

+ Hover me! +

+ + + ), +}; + +export const Hoverable: Story = { + args: { + place: TooltipPosition.Bottom, + delay: TooltipDelay.mediumDelay, + content: 'Tooltip Test', + hidden: false, + anchorSelect: '#hover-text', + }, + decorators: [ComponentDecorator], + render: ({ + anchorSelect, + className, + content, + delay, + noArrow, + offset, + place, + positionStrategy, + }) => ( + <> +

+ Hover me! +

+ + + ), +}; + +export const WithWidth: Story = { + args: { + place: TooltipPosition.Top, + delay: TooltipDelay.mediumDelay, + content: 'Tooltip with custom width', + hidden: false, + anchorSelect: '#width-text', + width: '200px', + }, + decorators: [ComponentDecorator], + render: ({ + anchorSelect, + className, + content, + delay, + noArrow, + offset, + place, + positionStrategy, + width, + }) => ( + <> +

+ Hover me to see custom width! +

+ + + ), +}; + +export const Catalog: CatalogStory = { + args: { hidden: false, content: 'Tooltip Test' }, + play: async ({ canvasElement }) => { + Object.values(TooltipPosition).forEach((position) => { + const element = canvasElement.querySelector( + `#${position}`, + ) as HTMLElement; + element.style.margin = '75px'; + }); + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + catalog: { + dimensions: [ + { + name: 'anchorSelect', + values: Object.values(TooltipPosition), + props: (anchorSelect: TooltipPosition) => ({ + anchorSelect: `#${anchorSelect}`, + place: anchorSelect, + }), + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/display/typography/components/H1Title.module.scss b/packages/twenty-ui/src/display/typography/components/H1Title.module.scss new file mode 100644 index 0000000000..cae007191c --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/H1Title.module.scss @@ -0,0 +1,19 @@ +.title { + font-size: var(--t-font-size-lg); + font-weight: var(--t-font-weight-semi-bold); + line-height: var(--t-text-line-height-md); + margin: 0; + margin-bottom: var(--t-spacing-4); +} + +.primary { + color: var(--t-font-color-primary); +} + +.secondary { + color: var(--t-font-color-secondary); +} + +.tertiary { + color: var(--t-font-color-tertiary); +} diff --git a/packages/twenty-ui/src/display/typography/components/H1Title.module.scss.d.ts b/packages/twenty-ui/src/display/typography/components/H1Title.module.scss.d.ts new file mode 100644 index 0000000000..d2e27896d4 --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/H1Title.module.scss.d.ts @@ -0,0 +1,7 @@ +declare const classNames: { + readonly title: 'title'; + readonly primary: 'primary'; + readonly secondary: 'secondary'; + readonly tertiary: 'tertiary'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/typography/components/H1Title.tsx b/packages/twenty-ui/src/display/typography/components/H1Title.tsx new file mode 100644 index 0000000000..b1fe71dd4f --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/H1Title.tsx @@ -0,0 +1,36 @@ +import { type ReactNode } from 'react'; +import { clsx } from 'clsx'; + +import styles from './H1Title.module.scss'; + +type H1TitleProps = { + title: ReactNode; + fontColor?: H1TitleFontColor; + className?: string; +}; + +export enum H1TitleFontColor { + Primary = 'primary', + Secondary = 'secondary', + Tertiary = 'tertiary', +} + +const fontColorClassNames: Record = { + [H1TitleFontColor.Primary]: styles.primary, + [H1TitleFontColor.Secondary]: styles.secondary, + [H1TitleFontColor.Tertiary]: styles.tertiary, +}; + +export const H1Title = ({ + title, + fontColor = H1TitleFontColor.Tertiary, + className, +}: H1TitleProps) => { + return ( +

+ {title} +

+ ); +}; diff --git a/packages/twenty-ui/src/display/typography/components/H2Title.module.scss b/packages/twenty-ui/src/display/typography/components/H2Title.module.scss new file mode 100644 index 0000000000..a0129ea151 --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/H2Title.module.scss @@ -0,0 +1,26 @@ +.container { + display: flex; + flex-direction: column; + margin-bottom: var(--t-spacing-4); +} + +.titleContainer { + align-items: center; + display: flex; + justify-content: space-between; +} + +.title { + color: var(--t-font-color-primary); + font-size: var(--t-font-size-md); + font-weight: var(--t-font-weight-semi-bold); + margin: 0; +} + +.description { + color: var(--t-font-color-tertiary); + font-size: var(--t-font-size-md); + font-weight: var(--t-font-weight-regular); + margin: 0; + margin-top: var(--t-spacing-2); +} diff --git a/packages/twenty-ui/src/display/typography/components/H2Title.module.scss.d.ts b/packages/twenty-ui/src/display/typography/components/H2Title.module.scss.d.ts new file mode 100644 index 0000000000..6f9a3f97b1 --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/H2Title.module.scss.d.ts @@ -0,0 +1,7 @@ +declare const classNames: { + readonly container: 'container'; + readonly titleContainer: 'titleContainer'; + readonly title: 'title'; + readonly description: 'description'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/typography/components/H2Title.tsx b/packages/twenty-ui/src/display/typography/components/H2Title.tsx new file mode 100644 index 0000000000..ceca604430 --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/H2Title.tsx @@ -0,0 +1,37 @@ +import { clsx } from 'clsx'; + +import { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip'; + +import styles from './H2Title.module.scss'; + +type H2TitleProps = { + title: string; + description?: string; + adornment?: React.ReactNode; + className?: string; +}; + +export const H2Title = ({ + title, + description, + adornment, + className, +}: H2TitleProps) => { + return ( +
+
+

{title}

+ {adornment} +
+ {description && ( +

+ +

+ )} +
+ ); +}; diff --git a/packages/twenty-ui/src/display/typography/components/H3Title.module.scss b/packages/twenty-ui/src/display/typography/components/H3Title.module.scss new file mode 100644 index 0000000000..a0751fdd94 --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/H3Title.module.scss @@ -0,0 +1,19 @@ +.container { + display: flex; + flex-direction: column; +} + +.title { + color: var(--t-font-color-primary); + font-size: var(--t-font-size-lg); + font-weight: var(--t-font-weight-semi-bold); + margin: 0; +} + +.description { + color: var(--t-font-color-tertiary); + font-size: var(--t-font-size-md); + font-weight: var(--t-font-weight-regular); + margin: 0; + margin-top: var(--t-spacing-2); +} diff --git a/packages/twenty-ui/src/display/typography/components/H3Title.module.scss.d.ts b/packages/twenty-ui/src/display/typography/components/H3Title.module.scss.d.ts new file mode 100644 index 0000000000..1b98f93c04 --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/H3Title.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly container: 'container'; + readonly title: 'title'; + readonly description: 'description'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/typography/components/H3Title.tsx b/packages/twenty-ui/src/display/typography/components/H3Title.tsx new file mode 100644 index 0000000000..9252d38e50 --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/H3Title.tsx @@ -0,0 +1,31 @@ +import { type ReactNode } from 'react'; +import { clsx } from 'clsx'; + +import { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip'; + +import styles from './H3Title.module.scss'; + +type H3TitleProps = { + title: ReactNode; + description?: string; + className?: string; +}; + +export const H3Title = ({ title, description, className }: H3TitleProps) => { + return ( +
+

{title}

+ {description && ( + // Design rule: Never set a description for H3 if there are any H2 in the page + // (in that case, each H2 must have its own description) +

+ +

+ )} +
+ ); +}; diff --git a/packages/twenty-ui/src/display/typography/components/Label.module.scss b/packages/twenty-ui/src/display/typography/components/Label.module.scss new file mode 100644 index 0000000000..21a490a5fa --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/Label.module.scss @@ -0,0 +1,12 @@ +.label { + color: var(--t-font-color-light); + font-weight: var(--t-font-weight-semi-bold); +} + +.default { + font-size: 11px; +} + +.small { + font-size: 9px; +} diff --git a/packages/twenty-ui/src/display/typography/components/Label.module.scss.d.ts b/packages/twenty-ui/src/display/typography/components/Label.module.scss.d.ts new file mode 100644 index 0000000000..17dd1e5c59 --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/Label.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly label: 'label'; + readonly default: 'default'; + readonly small: 'small'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/typography/components/Label.tsx b/packages/twenty-ui/src/display/typography/components/Label.tsx new file mode 100644 index 0000000000..c9c6c71d65 --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/Label.tsx @@ -0,0 +1,23 @@ +import { clsx } from 'clsx'; + +import styles from './Label.module.scss'; + +export type LabelVariant = 'default' | 'small'; + +type LabelProps = { + variant?: LabelVariant; + children?: React.ReactNode; + className?: string; +}; + +export const Label = ({ + variant = 'default', + children, + className, +}: LabelProps) => { + return ( +
+ {children} +
+ ); +}; diff --git a/packages/twenty-ui/src/display/typography/components/StyledText.module.scss b/packages/twenty-ui/src/display/typography/components/StyledText.module.scss new file mode 100644 index 0000000000..296c89c73e --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/StyledText.module.scss @@ -0,0 +1,28 @@ +.content { + font-size: var(--t-font-size-sm); + font-weight: var(--t-font-weight-regular); + + overflow: hidden; + padding-left: 0; + + white-space: nowrap; +} + +.wrapper { + --horizontal-padding: var(--t-spacing-1); + --vertical-padding: var(--t-spacing-2); + + cursor: initial; + + display: flex; + + flex-direction: row; + + font-size: var(--t-font-size-sm); + + gap: var(--t-spacing-2); + + padding: var(--vertical-padding) 0; + + color: var(--styled-text-color, var(--t-font-color-primary)); +} diff --git a/packages/twenty-ui/src/display/typography/components/StyledText.module.scss.d.ts b/packages/twenty-ui/src/display/typography/components/StyledText.module.scss.d.ts new file mode 100644 index 0000000000..98d24e6a05 --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/StyledText.module.scss.d.ts @@ -0,0 +1,5 @@ +declare const classNames: { + readonly content: 'content'; + readonly wrapper: 'wrapper'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/display/typography/components/StyledText.tsx b/packages/twenty-ui/src/display/typography/components/StyledText.tsx new file mode 100644 index 0000000000..404788a661 --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/StyledText.tsx @@ -0,0 +1,55 @@ +import { type ReactElement, type ReactNode } from 'react'; +import { clsx } from 'clsx'; + +import styles from './StyledText.module.scss'; + +type StyledTextProps = { + PrefixComponent?: ReactElement; + text: ReactNode; + color?: string; +}; + +type StyledTextContentProps = React.ComponentPropsWithoutRef<'div'>; + +export const StyledTextContent = ({ + className, + ...divProps +}: StyledTextContentProps) => ( + // oxlint-disable-next-line react/jsx-props-no-spreading +
+); + +type StyledTextWrapperProps = React.ComponentPropsWithoutRef<'div'> & { + color?: string; +}; + +export const StyledTextWrapper = ({ + color, + className, + style, + ...divProps +}: StyledTextWrapperProps) => ( +
+); + +export const StyledText = ({ + PrefixComponent, + text, + color, +}: StyledTextProps) => { + return ( + + {PrefixComponent ? PrefixComponent : null} + {text} + + ); +}; diff --git a/packages/twenty-ui/src/display/typography/components/__stories__/H1Title.stories.tsx b/packages/twenty-ui/src/display/typography/components/__stories__/H1Title.stories.tsx new file mode 100644 index 0000000000..9c7dd69a9d --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/__stories__/H1Title.stories.tsx @@ -0,0 +1,45 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { CatalogDecorator } from '@ui/testing/decorators/CatalogDecorator'; +import { ComponentDecorator } from '@ui/testing/decorators/ComponentDecorator'; +import { type CatalogStory } from '@ui/testing/types/CatalogStory'; + +import { H1Title, H1TitleFontColor } from '../H1Title'; + +const meta: Meta = { + title: 'UI/Display/Typography/Title/H1Title', + component: H1Title, + decorators: [ComponentDecorator], +}; + +export default meta; + +type Story = StoryObj; + +const args = { + title: 'Title', + fontColor: H1TitleFontColor.Primary, +}; + +export const Default: Story = { + args, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args, + decorators: [CatalogDecorator], + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + catalog: { + dimensions: [ + { + name: 'FontColor', + values: Object.values(H1TitleFontColor), + props: (fontColor: H1TitleFontColor) => ({ fontColor }), + }, + ], + }, + }, +}; diff --git a/packages/twenty-ui/src/display/typography/components/__stories__/H2Title.stories.tsx b/packages/twenty-ui/src/display/typography/components/__stories__/H2Title.stories.tsx new file mode 100644 index 0000000000..b936452bc0 --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/__stories__/H2Title.stories.tsx @@ -0,0 +1,34 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { ComponentDecorator } from '@ui/testing/decorators/ComponentDecorator'; + +import { H2Title } from '../H2Title'; + +const args = { + title: 'Sub title', + description: 'Lorem ipsum dolor sit amet', +}; + +const meta: Meta = { + title: 'UI/Display/Typography/Title/H2Title', + component: H2Title, + decorators: [ComponentDecorator], + args: { + title: args.title, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + decorators: [ComponentDecorator], +}; + +export const WithDescription: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args, + decorators: [ComponentDecorator], +}; diff --git a/packages/twenty-ui/src/display/typography/components/__stories__/H3Title.stories.tsx b/packages/twenty-ui/src/display/typography/components/__stories__/H3Title.stories.tsx new file mode 100644 index 0000000000..78286472d7 --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/__stories__/H3Title.stories.tsx @@ -0,0 +1,22 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { ComponentDecorator } from '@ui/testing/decorators/ComponentDecorator'; + +import { H3Title } from '../H3Title'; + +const meta: Meta = { + title: 'UI/Display/Typography/Title/H3Title', + component: H3Title, + decorators: [ComponentDecorator], + args: { + title: 'H3 title', + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + decorators: [ComponentDecorator], +}; diff --git a/packages/twenty-ui/src/display/typography/components/__stories__/Label.stories.tsx b/packages/twenty-ui/src/display/typography/components/__stories__/Label.stories.tsx new file mode 100644 index 0000000000..3497371d28 --- /dev/null +++ b/packages/twenty-ui/src/display/typography/components/__stories__/Label.stories.tsx @@ -0,0 +1,46 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { CatalogDecorator } from '@ui/testing/decorators/CatalogDecorator'; +import { ComponentDecorator } from '@ui/testing/decorators/ComponentDecorator'; +import { type CatalogStory } from '@ui/testing/types/CatalogStory'; + +import { Label, type LabelVariant } from '../Label'; + +const meta: Meta = { + title: 'UI/Display/Typography/Label', + component: Label, + decorators: [ComponentDecorator], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + decorators: [ComponentDecorator], + args: { + children: 'Label', + }, +}; + +export const Catalog: CatalogStory = { + decorators: [CatalogDecorator], + args: { + children: 'Label', + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + catalog: { + dimensions: [ + { + name: 'Variant', + values: ['default', 'small'] satisfies LabelVariant[], + props: (variant: LabelVariant) => ({ variant }), + }, + ], + }, + }, +}; diff --git a/packages/twenty-ui/src/feedback/index.ts b/packages/twenty-ui/src/feedback/index.ts index a3046804c0..dc1e659331 100644 --- a/packages/twenty-ui/src/feedback/index.ts +++ b/packages/twenty-ui/src/feedback/index.ts @@ -7,4 +7,11 @@ * |___/ */ -export {}; +export { Loader } from './loader/components/Loader'; +export { CircularProgressBar } from './progress-bar/components/CircularProgressBar'; +export type { + ProgressBarProps, + StyledBarProps, +} from './progress-bar/components/ProgressBar'; +export { ProgressBar } from './progress-bar/components/ProgressBar'; +export { useProgressAnimation } from './progress-bar/hooks/useProgressAnimation'; diff --git a/packages/twenty-ui/src/feedback/loader/__stories__/Loader.stories.tsx b/packages/twenty-ui/src/feedback/loader/__stories__/Loader.stories.tsx new file mode 100644 index 0000000000..2314ee5ea1 --- /dev/null +++ b/packages/twenty-ui/src/feedback/loader/__stories__/Loader.stories.tsx @@ -0,0 +1,96 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { expect, waitFor } from 'storybook/test'; + +import { ComponentDecorator } from '@ui/testing'; + +import { Loader } from '../components/Loader'; + +const meta: Meta = { + title: 'UI/Feedback/Loader', + component: Loader, + decorators: [ComponentDecorator], +}; + +export default meta; + +type Story = StoryObj; + +export const WithColor: Story = { + args: { + color: 'red', + }, + play: async ({ canvasElement }) => { + await waitFor(() => { + const element = canvasElement.querySelector(':first-child'); + + expect(element).toBeVisible(); + + return element; + }); + }, +}; + +export const WithDefaultCssVariable: Story = { + decorators: [ + (Story) => ( + // @ts-expect-error: Custom CSS variable for demonstration purposes +
+ +
+ ), + ], + play: async ({ canvasElement }) => { + await waitFor(() => { + const element = canvasElement.querySelector(':first-child'); + + expect(element).toBeVisible(); + + return element; + }); + }, +}; + +export const WithDefaultColor: Story = { + play: async ({ canvasElement }) => { + await waitFor(() => { + const element = canvasElement.querySelector(':first-child'); + + expect(element).toBeVisible(); + + return element; + }); + }, +}; + +export const WithDifferentColors: Story = { + render: () => ( +
+ + + + +
+ ), + play: async ({ canvasElement }) => { + const loaders = await waitFor(() => { + const elements = canvasElement.querySelectorAll('#container > *'); + + expect(elements).toHaveLength(4); + + return elements; + }); + + expect(loaders[0]).toHaveStyle({ + borderColor: expect.stringContaining('red'), + }); + expect(loaders[1]).toHaveStyle({ + borderColor: expect.stringContaining('blue'), + }); + expect(loaders[2]).toHaveStyle({ + borderColor: expect.stringContaining('yellow'), + }); + expect(loaders[3]).toHaveStyle({ + borderColor: expect.stringContaining('green'), + }); + }, +}; diff --git a/packages/twenty-ui/src/feedback/loader/components/Loader.module.scss b/packages/twenty-ui/src/feedback/loader/components/Loader.module.scss new file mode 100644 index 0000000000..81888e2b40 --- /dev/null +++ b/packages/twenty-ui/src/feedback/loader/components/Loader.module.scss @@ -0,0 +1,47 @@ +.container { + box-sizing: border-box; + justify-content: center; + align-items: center; + display: flex; + gap: var(--t-spacing-2); + width: var(--t-spacing-6); + height: var(--t-spacing-3); + border-radius: var(--t-border-radius-pill); + border: 1px solid + var(--loader-color, var(--tw-button-color, var(--t-font-color-tertiary))); + overflow: hidden; +} + +.dot { + background-color: var( + --loader-color, + var(--tw-button-color, var(--t-font-color-tertiary)) + ); + border-radius: var(--t-border-radius-pill); + height: 8px; + width: 8px; + animation: loaderDot 0.8s ease-in-out infinite; +} + +// Replicates the framer keyframes: x [-16, 0, 16], width [8, 12, 8], +// height [8, 2, 8] with times [0, 0.15, 0.3] over a 0.8s loop. +@keyframes loaderDot { + 0% { + transform: translateX(-16px); + width: 8px; + height: 8px; + } + + 15% { + transform: translateX(0); + width: 12px; + height: 2px; + } + + 30%, + 100% { + transform: translateX(16px); + width: 8px; + height: 8px; + } +} diff --git a/packages/twenty-ui/src/feedback/loader/components/Loader.module.scss.d.ts b/packages/twenty-ui/src/feedback/loader/components/Loader.module.scss.d.ts new file mode 100644 index 0000000000..d926ea2493 --- /dev/null +++ b/packages/twenty-ui/src/feedback/loader/components/Loader.module.scss.d.ts @@ -0,0 +1,5 @@ +declare const classNames: { + readonly container: 'container'; + readonly dot: 'dot'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/feedback/loader/components/Loader.tsx b/packages/twenty-ui/src/feedback/loader/components/Loader.tsx new file mode 100644 index 0000000000..bc8a4ae4b6 --- /dev/null +++ b/packages/twenty-ui/src/feedback/loader/components/Loader.tsx @@ -0,0 +1,25 @@ +import { type ThemeColor } from '@ui/theme'; +import { themeCssVariables } from '@ui/theme-constants'; + +import styles from './Loader.module.scss'; + +type LoaderProps = { + color?: ThemeColor; +}; + +export const Loader = ({ color }: LoaderProps) => { + return ( +
+
+
+ ); +}; diff --git a/packages/twenty-ui/src/feedback/progress-bar/components/CircularProgressBar.tsx b/packages/twenty-ui/src/feedback/progress-bar/components/CircularProgressBar.tsx new file mode 100644 index 0000000000..9964b49106 --- /dev/null +++ b/packages/twenty-ui/src/feedback/progress-bar/components/CircularProgressBar.tsx @@ -0,0 +1,71 @@ +import { motion, useAnimation } from 'framer-motion'; +import { useEffect, useMemo } from 'react'; + +type CircularProgressBarProps = { + size?: number; + barWidth?: number; + barColor?: string; +}; + +export const CircularProgressBar = ({ + size = 50, + barWidth = 5, + barColor = 'currentColor', +}: CircularProgressBarProps) => { + const controls = useAnimation(); + + const circumference = useMemo( + () => 2 * Math.PI * (size / 2 - barWidth), + [size, barWidth], + ); + + useEffect(() => { + const animateIndeterminate = async () => { + const baseSegment = Math.max(5, circumference / 10); // Adjusting for smaller values + + // Adjusted sequence based on baseSegment + const dashSequences = [ + `${baseSegment} ${circumference - baseSegment}`, + `${baseSegment * 2} ${circumference - baseSegment * 2}`, + `${baseSegment * 3} ${circumference - baseSegment * 3}`, + `${baseSegment * 2} ${circumference - baseSegment * 2}`, + `${baseSegment} ${circumference - baseSegment}`, + ]; + + await controls.start({ + strokeDasharray: dashSequences, + rotate: [0, 720], + transition: { + strokeDasharray: { + duration: 2, + ease: 'linear', + repeat: Infinity, + repeatType: 'loop', + }, + rotate: { + duration: 2, + ease: 'linear', + repeat: Infinity, + repeatType: 'loop', + }, + }, + }); + }; + + animateIndeterminate(); + }, [circumference, controls]); + + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/feedback/progress-bar/components/ProgressBar.module.scss b/packages/twenty-ui/src/feedback/progress-bar/components/ProgressBar.module.scss new file mode 100644 index 0000000000..cbb0c72a73 --- /dev/null +++ b/packages/twenty-ui/src/feedback/progress-bar/components/ProgressBar.module.scss @@ -0,0 +1,22 @@ +.bar { + height: var(--t-spacing-2); + background-color: var(--progress-bar-background-color, none); + border-radius: 0; + overflow: hidden; + width: 100%; + + &[data-with-border-radius] { + border-radius: var(--t-border-radius-xxl); + } +} + +.barFilling { + background-color: var(--progress-bar-color, var(--t-font-color-primary)); + border-radius: 0; + height: 100%; + width: 100%; + + &[data-with-border-radius] { + border-radius: var(--t-border-radius-md); + } +} diff --git a/packages/twenty-ui/src/feedback/progress-bar/components/ProgressBar.module.scss.d.ts b/packages/twenty-ui/src/feedback/progress-bar/components/ProgressBar.module.scss.d.ts new file mode 100644 index 0000000000..9ced34bf10 --- /dev/null +++ b/packages/twenty-ui/src/feedback/progress-bar/components/ProgressBar.module.scss.d.ts @@ -0,0 +1,5 @@ +declare const classNames: { + readonly bar: 'bar'; + readonly barFilling: 'barFilling'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/feedback/progress-bar/components/ProgressBar.tsx b/packages/twenty-ui/src/feedback/progress-bar/components/ProgressBar.tsx new file mode 100644 index 0000000000..ba531cb11f --- /dev/null +++ b/packages/twenty-ui/src/feedback/progress-bar/components/ProgressBar.tsx @@ -0,0 +1,58 @@ +import { clsx } from 'clsx'; + +import styles from './ProgressBar.module.scss'; + +export type ProgressBarProps = { + value: number; + className?: string; + barColor?: string; + backgroundColor?: string; + withBorderRadius?: boolean; +}; + +export type StyledBarProps = { + className?: string; + backgroundColor?: string; + withBorderRadius?: boolean; +}; + +const MIN_BAR_WIDTH_PX = 12; + +export const ProgressBar = ({ + value, + className, + barColor, + backgroundColor = 'none', + withBorderRadius = false, +}: ProgressBarProps) => ( +
+
0 ? MIN_BAR_WIDTH_PX : 0, + width: `${Math.ceil(value)}%`, + transition: 'width 0.3s linear', + }} + > +
+
+
+); diff --git a/packages/twenty-ui/src/feedback/progress-bar/components/__stories__/CircularProgressBar.stories.tsx b/packages/twenty-ui/src/feedback/progress-bar/components/__stories__/CircularProgressBar.stories.tsx new file mode 100644 index 0000000000..36dc05b944 --- /dev/null +++ b/packages/twenty-ui/src/feedback/progress-bar/components/__stories__/CircularProgressBar.stories.tsx @@ -0,0 +1,56 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { CatalogDecorator, type CatalogStory } from '@ui/testing'; + +import { ComponentDecorator } from '@ui/testing/decorators/ComponentDecorator'; +import { CircularProgressBar } from '../CircularProgressBar'; + +const meta: Meta = { + title: 'UI/Feedback/CircularProgressBar/CircularProgressBar', + component: CircularProgressBar, + args: { + size: 50, + }, + parameters: { + chromatic: { disableSnapshot: true }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + argTypes: {}, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + catalog: { + dimensions: [ + { + name: 'barColor', + values: [undefined, 'red'], + props: (barColor: string) => ({ barColor }), + labels: (color: string) => `Segment Color: ${color ?? 'default'}`, + }, + { + name: 'barWidth', + values: [undefined, 5, 10], + props: (barWidth: number) => ({ barWidth }), + labels: (width: number) => + `Stroke Width: ${width ? width + ' px' : 'default'}`, + }, + { + name: 'size', + values: [undefined, 80, 30], + props: (size: number) => ({ size }), + labels: (size: number) => `Size: ${size ? size + ' px' : 'default'}`, + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/feedback/progress-bar/components/__stories__/ProgressBar.stories.tsx b/packages/twenty-ui/src/feedback/progress-bar/components/__stories__/ProgressBar.stories.tsx new file mode 100644 index 0000000000..04d1cabd42 --- /dev/null +++ b/packages/twenty-ui/src/feedback/progress-bar/components/__stories__/ProgressBar.stories.tsx @@ -0,0 +1,50 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { useProgressAnimation } from '@ui/feedback/progress-bar/hooks/useProgressAnimation'; +import { ComponentDecorator } from '@ui/testing/decorators/ComponentDecorator'; +import { ProgressBar } from '../ProgressBar'; + +const meta: Meta = { + title: 'UI/Feedback/ProgressBar/ProgressBar', + component: ProgressBar, + decorators: [ComponentDecorator], + argTypes: { + className: { control: false }, + value: { control: { type: 'range', min: 0, max: 100, step: 1 } }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: 75, + }, +}; + +export const Animated: Story = { + tags: ['!test'], + argTypes: { + value: { control: false }, + }, + decorators: [ + (Story) => { + const { value } = useProgressAnimation({ + autoPlay: true, + initialValue: 0, + finalValue: 100, + options: { + duration: 10000, + }, + }); + + return ; + }, + ], + parameters: { + chromatic: { disableSnapshot: true }, + }, +}; diff --git a/packages/twenty-ui/src/feedback/progress-bar/hooks/useProgressAnimation.ts b/packages/twenty-ui/src/feedback/progress-bar/hooks/useProgressAnimation.ts new file mode 100644 index 0000000000..fa99cdbb5b --- /dev/null +++ b/packages/twenty-ui/src/feedback/progress-bar/hooks/useProgressAnimation.ts @@ -0,0 +1,57 @@ +import { millisecondsToSeconds } from 'date-fns'; +import { + animate, + type AnimationPlaybackControls, + type ValueAnimationTransition, +} from 'framer-motion'; +import { useCallback, useEffect, useState } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +export const useProgressAnimation = ({ + autoPlay = true, + initialValue = 0, + finalValue = 100, + options, +}: { + autoPlay?: boolean; + initialValue?: number; + finalValue?: number; + options?: ValueAnimationTransition; +}) => { + const [animation, setAnimation] = useState< + AnimationPlaybackControls | undefined + >(); + const [value, setValue] = useState(initialValue); + + const startAnimation = useCallback(() => { + if (isDefined(animation)) return; + + const duration = isDefined(options?.duration) + ? millisecondsToSeconds(options.duration) + : undefined; + + setAnimation( + animate(initialValue, finalValue, { + ...options, + duration, + onUpdate: (nextValue) => { + if (value === nextValue) return; + setValue(nextValue); + options?.onUpdate?.(nextValue); + }, + }), + ); + }, [animation, finalValue, initialValue, options, value]); + + useEffect(() => { + if (autoPlay && !animation) { + startAnimation(); + } + }, [animation, autoPlay, startAnimation]); + + return { + animation, + startAnimation, + value, + }; +}; diff --git a/packages/twenty-ui/src/input/button/components/AnimatedButton.module.scss b/packages/twenty-ui/src/input/button/components/AnimatedButton.module.scss new file mode 100644 index 0000000000..4f4d52abd2 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/AnimatedButton.module.scss @@ -0,0 +1,129 @@ +// Static rules from the legacy Linaria StyledButton. The dynamic +// variant/accent/inverted/disabled/focus colors stay computed at runtime in +// AnimatedButton.tsx (computeAnimatedButtonDynamicStyles) and arrive through +// the --abtn-* inline custom properties, exactly like the deprecated source. +.button { + align-items: center; + background: var(--abtn-bg); + border-color: var(--abtn-border-color); + border-radius: var(--abtn-radius, var(--t-border-radius-sm)); + border-style: solid; + border-width: var(--abtn-border-width); + box-shadow: var(--abtn-box-shadow); + color: var(--abtn-color); + cursor: pointer; + display: flex; + flex-direction: row; + font-family: var(--t-font-family); + font-size: var(--t-font-size-md); + font-weight: 500; + gap: var(--t-spacing-1); + justify-content: var(--abtn-justify, flex-start); + padding: 0 var(--t-spacing-2); + text-decoration: none; + transition: background 0.1s ease; + white-space: nowrap; + width: auto; + + &:hover { + background: var(--abtn-hover-bg); + } + + &:active { + background: var(--abtn-active-bg); + } + + &:focus { + outline: none; + } + + &[data-disabled] { + cursor: not-allowed; + } +} + +// position only reshapes the radius; standalone uses the .button fallback. +// These rules only assign the custom property so the border-radius +// declaration stays on the flat .button class for overridability. +.button[data-position='left'] { + --abtn-radius: var(--t-border-radius-sm) 0px 0px var(--t-border-radius-sm); +} + +.button[data-position='middle'] { + --abtn-radius: 0px; +} + +.button[data-position='right'] { + --abtn-radius: 0px var(--t-border-radius-sm) var(--t-border-radius-sm) 0px; +} + +// --abtn-square-size keeps the square width declaration flat on .square. +.small { + --abtn-square-size: 24px; + + height: 24px; +} + +.medium { + --abtn-square-size: 32px; + + height: 32px; +} + +.fullWidth { + width: 100%; +} + +// Square icon-only mode. Declared after .button and .fullWidth so its +// justify-content, padding and width win the cascade at equal specificity, +// matching the legacy ternary precedence (square beats fullWidth). +.square { + justify-content: center; + padding: 0; + width: var(--abtn-square-size); +} + +.motion { + display: flex; +} + +.separator { + background: var(--t-font-color-light); + height: var(--t-spacing-4); + margin: 0; + width: 1px; + + &[data-size='small'] { + height: var(--t-spacing-2); + } + + &[data-accent='blue'] { + background: var(--t-border-color-blue); + } + + &[data-accent='danger'] { + background: var(--t-border-color-danger); + } +} + +.shortcutLabel { + color: var(--t-font-color-light); + font-weight: var(--t-font-weight-medium); + + &[data-accent='blue'] { + color: var(--t-border-color-blue); + } + + &[data-accent='danger'] { + color: var(--t-color-red8); + } + + &[data-accent='danger'][data-variant='primary'] { + color: var(--t-border-color-danger); + } +} + +.soonPillContainer { + display: flex; + margin-left: auto; +} diff --git a/packages/twenty-ui/src/input/button/components/AnimatedButton.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/AnimatedButton.module.scss.d.ts new file mode 100644 index 0000000000..77d187e2b1 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/AnimatedButton.module.scss.d.ts @@ -0,0 +1,12 @@ +declare const classNames: { + readonly button: 'button'; + readonly small: 'small'; + readonly medium: 'medium'; + readonly fullWidth: 'fullWidth'; + readonly square: 'square'; + readonly motion: 'motion'; + readonly separator: 'separator'; + readonly shortcutLabel: 'shortcutLabel'; + readonly soonPillContainer: 'soonPillContainer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/AnimatedButton.tsx b/packages/twenty-ui/src/input/button/components/AnimatedButton.tsx new file mode 100644 index 0000000000..59236332da --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/AnimatedButton.tsx @@ -0,0 +1,436 @@ +import { clsx } from 'clsx'; +import { type MotionProps, motion } from 'framer-motion'; +import React, { useContext, useMemo } from 'react'; +import { Link } from 'react-router-dom'; + +import { Pill } from '@ui/components/Pill/Pill'; +import { ThemeContext, themeCssVariables } from '@ui/theme-constants'; +import { GRAY_SCALE_LIGHT } from '@ui/theme/constants/GrayScaleLight'; +import { useIsMobile } from '@ui/utilities'; +import { getOsShortcutSeparator } from '@ui/utilities/device/getOsShortcutSeparator'; +import { + type ButtonAccent, + type ButtonPosition, + type ButtonProps, + type ButtonVariant, +} from './Button/Button'; + +import styles from './AnimatedButton.module.scss'; + +export type AnimatedButtonProps = ButtonProps & + Pick & { + animatedSvg: React.ReactNode; + soonLabel?: string; + // Renders a square icon-only button (width matches the size-based height). + square?: boolean; + }; + +type AnimatedButtonDynamicStyles = { + background: string; + borderColor: string; + borderWidthOverride: string; + boxShadow: string; + color: string; + hoverBackground: string; + activeBackground: string; +}; + +const computeAnimatedButtonDynamicStyles = ( + variant: ButtonVariant, + inverted: boolean, + accent: ButtonAccent, + disabled: boolean, + focus: boolean, + position: ButtonPosition, +): AnimatedButtonDynamicStyles => { + const result: AnimatedButtonDynamicStyles = { + background: 'transparent', + borderColor: 'transparent', + borderWidthOverride: '', + boxShadow: 'none', + color: themeCssVariables.font.color.secondary, + hoverBackground: 'transparent', + activeBackground: 'transparent', + }; + + switch (variant) { + case 'primary': + switch (accent) { + case 'default': + result.background = !inverted + ? themeCssVariables.background.secondary + : themeCssVariables.background.primary; + result.borderColor = !inverted + ? !disabled && focus + ? themeCssVariables.color.blue + : themeCssVariables.background.transparent.light + : themeCssVariables.background.transparent.light; + result.borderWidthOverride = '1px 1px 1px 1px'; + result.boxShadow = + !disabled && focus + ? `0 0 0 3px ${ + !inverted + ? themeCssVariables.accent.tertiary + : themeCssVariables.background.transparent.medium + }` + : 'none'; + result.color = !inverted + ? !disabled + ? themeCssVariables.font.color.secondary + : themeCssVariables.font.color.extraLight + : themeCssVariables.font.color.secondary; + if (!disabled) { + result.hoverBackground = !inverted + ? themeCssVariables.background.tertiary + : themeCssVariables.background.secondary; + result.activeBackground = !inverted + ? themeCssVariables.background.quaternary + : themeCssVariables.background.tertiary; + } else { + result.hoverBackground = result.background; + result.activeBackground = result.background; + } + break; + case 'blue': + result.background = !inverted + ? themeCssVariables.color.blue + : themeCssVariables.background.primary; + result.borderColor = !inverted + ? focus + ? themeCssVariables.color.blue + : themeCssVariables.background.transparent.light + : themeCssVariables.background.transparent.light; + result.borderWidthOverride = '1px 1px 1px 1px'; + result.boxShadow = + !disabled && focus + ? `0 0 0 3px ${ + !inverted + ? themeCssVariables.accent.tertiary + : themeCssVariables.background.transparent.medium + }` + : 'none'; + result.color = !inverted + ? GRAY_SCALE_LIGHT.gray1 + : themeCssVariables.color.blue; + if (!disabled) { + result.hoverBackground = !inverted + ? themeCssVariables.color.blue10 + : themeCssVariables.background.secondary; + result.activeBackground = !inverted + ? themeCssVariables.color.blue12 + : themeCssVariables.background.tertiary; + } else { + result.hoverBackground = result.background; + result.activeBackground = result.background; + } + break; + case 'danger': + result.background = !inverted + ? themeCssVariables.color.red + : themeCssVariables.background.primary; + result.borderColor = !inverted + ? focus + ? themeCssVariables.color.red + : themeCssVariables.background.transparent.light + : themeCssVariables.background.transparent.light; + result.borderWidthOverride = '1px 1px'; + result.boxShadow = + !disabled && focus + ? `0 0 0 3px ${ + !inverted + ? themeCssVariables.color.red3 + : themeCssVariables.background.transparent.medium + }` + : 'none'; + result.color = !inverted + ? themeCssVariables.background.primary + : themeCssVariables.color.red; + if (!disabled) { + result.hoverBackground = !inverted + ? themeCssVariables.color.red8 + : themeCssVariables.background.secondary; + result.activeBackground = !inverted + ? themeCssVariables.color.red10 + : themeCssVariables.background.tertiary; + } else { + result.hoverBackground = result.background; + result.activeBackground = result.background; + } + break; + } + break; + case 'secondary': + case 'tertiary': + switch (accent) { + case 'default': + result.background = 'transparent'; + result.borderColor = !inverted + ? variant === 'secondary' + ? !disabled && focus + ? themeCssVariables.color.blue + : themeCssVariables.background.transparent.medium + : focus + ? themeCssVariables.color.blue + : 'transparent' + : variant === 'secondary' + ? focus || disabled + ? GRAY_SCALE_LIGHT.gray1 + : themeCssVariables.background.transparent.primary + : focus + ? GRAY_SCALE_LIGHT.gray1 + : 'transparent'; + result.borderWidthOverride = '1px 1px 1px 1px'; + result.boxShadow = + !disabled && focus + ? `0 0 0 3px ${ + !inverted + ? themeCssVariables.accent.tertiary + : themeCssVariables.background.transparent.medium + }` + : 'none'; + result.color = !inverted + ? !disabled + ? themeCssVariables.font.color.secondary + : themeCssVariables.font.color.extraLight + : themeCssVariables.font.color.inverted; + result.hoverBackground = !inverted + ? !disabled + ? themeCssVariables.background.transparent.light + : 'transparent' + : themeCssVariables.background.transparent.light; + result.activeBackground = !inverted + ? !disabled + ? themeCssVariables.background.transparent.light + : 'transparent' + : themeCssVariables.background.transparent.medium; + break; + case 'blue': + result.background = 'transparent'; + result.borderColor = !inverted + ? variant === 'secondary' + ? focus + ? themeCssVariables.color.blue + : themeCssVariables.accent.primary + : focus + ? themeCssVariables.color.blue + : 'transparent' + : variant === 'secondary' + ? focus || disabled + ? GRAY_SCALE_LIGHT.gray1 + : themeCssVariables.background.transparent.primary + : focus + ? GRAY_SCALE_LIGHT.gray1 + : 'transparent'; + result.borderWidthOverride = '1px 1px 1px 1px'; + result.boxShadow = + !disabled && focus + ? `0 0 0 3px ${ + !inverted + ? themeCssVariables.accent.tertiary + : themeCssVariables.background.transparent.medium + }` + : 'none'; + result.color = !inverted + ? !disabled + ? themeCssVariables.color.blue + : themeCssVariables.accent.accent4060 + : themeCssVariables.font.color.inverted; + result.hoverBackground = !inverted + ? !disabled + ? themeCssVariables.accent.tertiary + : 'transparent' + : themeCssVariables.background.transparent.light; + result.activeBackground = !inverted + ? !disabled + ? themeCssVariables.accent.secondary + : 'transparent' + : themeCssVariables.background.transparent.medium; + break; + case 'danger': + result.background = 'transparent'; + result.borderColor = !inverted + ? variant === 'secondary' + ? focus + ? themeCssVariables.color.red + : themeCssVariables.border.color.danger + : focus + ? themeCssVariables.color.red + : 'transparent' + : variant === 'secondary' + ? focus || disabled + ? GRAY_SCALE_LIGHT.gray1 + : themeCssVariables.background.transparent.primary + : focus + ? GRAY_SCALE_LIGHT.gray1 + : 'transparent'; + result.borderWidthOverride = '1px 1px 1px 1px'; + result.boxShadow = + !disabled && focus + ? `0 0 0 3px ${ + !inverted + ? themeCssVariables.color.red3 + : themeCssVariables.background.transparent.medium + }` + : 'none'; + result.color = !inverted + ? themeCssVariables.font.color.danger + : themeCssVariables.font.color.inverted; + result.hoverBackground = !inverted + ? !disabled + ? themeCssVariables.background.danger + : 'transparent' + : themeCssVariables.background.transparent.light; + result.activeBackground = !inverted + ? !disabled + ? themeCssVariables.background.danger + : 'transparent' + : themeCssVariables.background.transparent.medium; + break; + } + break; + } + + if (result.borderWidthOverride !== '' && position !== 'standalone') { + switch (position) { + case 'left': + result.borderWidthOverride = '1px 0px 1px 1px'; + break; + case 'middle': + result.borderWidthOverride = '1px 0px 1px 0px'; + break; + case 'right': + result.borderWidthOverride = '1px 1px 1px 0px'; + break; + } + } + + return result; +}; + +export const AnimatedButton = ({ + className, + Icon, + animatedSvg, + title, + fullWidth = false, + variant = 'primary', + inverted = false, + size = 'medium', + accent = 'default', + square = false, + position = 'standalone', + soon = false, + disabled = false, + justify = 'flex-start', + focus = false, + onClick, + to, + target, + dataTestId, + hotkeys, + ariaLabel, + animate, + transition, + dataClickOutsideId, + dataGloballyPreventClickOutside, + soonLabel = 'Soon', +}: AnimatedButtonProps) => { + const { theme } = useContext(ThemeContext); + const isMobile = useIsMobile(); + const isDisabled = soon || disabled; + + const dynamicStyles = useMemo(() => { + const computedStyles = computeAnimatedButtonDynamicStyles( + variant, + inverted, + accent, + isDisabled, + focus, + position, + ); + return { + '--abtn-bg': computedStyles.background, + '--abtn-border-color': computedStyles.borderColor, + '--abtn-border-width': computedStyles.borderWidthOverride || undefined, + '--abtn-box-shadow': computedStyles.boxShadow, + '--abtn-color': computedStyles.color, + '--abtn-hover-bg': computedStyles.hoverBackground, + '--abtn-active-bg': computedStyles.activeBackground, + } as React.CSSProperties; + }, [variant, inverted, accent, isDisabled, focus, position]); + + // Replaces the legacy Linaria `as` polymorphism: react-router Link when a + // `to` is provided, a native button otherwise. Typed as any to forward all + // props untyped, exactly like the legacy `as` prop did. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ButtonComponent: any = to ? Link : 'button'; + + return ( + + {Icon && ( + + + + )} + {animatedSvg && ( + + {animatedSvg} + + )} + {title} + {hotkeys && !isMobile && ( + <> +
+
+ {hotkeys.join(getOsShortcutSeparator())} +
+ + )} + {soon && ( + + + + )} + + ); +}; diff --git a/packages/twenty-ui/src/input/button/components/AnimatedLightIconButton.module.scss b/packages/twenty-ui/src/input/button/components/AnimatedLightIconButton.module.scss new file mode 100644 index 0000000000..d2e16f65d7 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/AnimatedLightIconButton.module.scss @@ -0,0 +1,77 @@ +// Color/border/shadow rules replicate the legacy Linaria ternaries. Order +// matters at equal specificity: accent base, then disabled, then focus and +// active last (active || focus beats disabled, matching the legacy switch). +.button { + align-items: center; + background: transparent; + border: none; + border-radius: var(--t-border-radius-sm); + box-shadow: none; + color: var(--t-font-color-secondary); + cursor: pointer; + display: flex; + flex-direction: row; + font-family: var(--t-font-family); + font-weight: var(--t-font-weight-regular); + gap: var(--t-spacing-1); + justify-content: center; + padding: var(--t-spacing-1); + transition: background 0.1s ease; + white-space: nowrap; + + &:hover { + background: var(--t-background-transparent-light); + } + + &:focus { + outline: none; + } + + &:active { + background: var(--t-background-transparent-medium); + } + + &[data-accent='tertiary'] { + color: var(--t-font-color-tertiary); + } + + &[data-disabled] { + color: var(--t-font-color-extra-light); + cursor: not-allowed; + + &:hover, + &:active { + background: transparent; + } + } + + // data-focus is only set when enabled, so disabled+focus stays unstyled, + // matching the legacy focus={focus && !disabled} gating. + &[data-focus] { + border: 1px solid var(--t-color-blue); + box-shadow: 0 0 0 3px var(--t-color-blue3); + color: var(--t-color-blue); + } + + &[data-active] { + color: var(--t-color-blue); + } +} + +.small { + height: 24px; + min-width: 24px; + width: 24px; +} + +.medium { + height: 32px; + min-width: 32px; + width: 32px; +} + +.iconContainer { + align-items: center; + display: flex; + justify-content: center; +} diff --git a/packages/twenty-ui/src/input/button/components/AnimatedLightIconButton.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/AnimatedLightIconButton.module.scss.d.ts new file mode 100644 index 0000000000..8acffa9bd7 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/AnimatedLightIconButton.module.scss.d.ts @@ -0,0 +1,7 @@ +declare const classNames: { + readonly button: 'button'; + readonly small: 'small'; + readonly medium: 'medium'; + readonly iconContainer: 'iconContainer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/AnimatedLightIconButton.tsx b/packages/twenty-ui/src/input/button/components/AnimatedLightIconButton.tsx new file mode 100644 index 0000000000..7828b847f4 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/AnimatedLightIconButton.tsx @@ -0,0 +1,71 @@ +import { clsx } from 'clsx'; +import { motion, type MotionProps } from 'framer-motion'; +import { type ComponentProps, type MouseEvent, useContext } from 'react'; + +import { type IconComponent } from '@ui/display'; +import { + type LightIconButtonAccent, + type LightIconButtonSize, +} from '@ui/input/button/components/LightIconButton'; +import { ThemeContext } from '@ui/theme-constants'; + +import styles from './AnimatedLightIconButton.module.scss'; + +export type AnimatedLightIconButtonProps = { + className?: string; + testId?: string; + Icon?: IconComponent; + title?: string; + size?: LightIconButtonSize; + accent?: LightIconButtonAccent; + active?: boolean; + disabled?: boolean; + focus?: boolean; + onClick?: (event: MouseEvent) => void; +} & Pick, 'aria-label' | 'title'> & + Pick; + +export const AnimatedLightIconButton = ({ + 'aria-label': ariaLabel, + className, + testId, + animate, + transition, + Icon, + active = false, + size = 'small', + accent = 'secondary', + disabled = false, + focus = false, + onClick, + title, +}: AnimatedLightIconButtonProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/input/button/components/Button/Button.module.scss b/packages/twenty-ui/src/input/button/components/Button/Button.module.scss new file mode 100644 index 0000000000..aced1cab6d --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/Button.module.scss @@ -0,0 +1,322 @@ +@use 'sass:map'; + +// GRAY_SCALE_LIGHT.gray1 from theme/constants/GrayScaleLight.ts — the legacy +// Linaria styles read it from JS because it is theme-invariant. +$gray-scale-light-gray1: color(display-p3 1 1 1); + +// The variant × accent × inverted × disabled × focus matrix below is ported +// token-for-token from computeButtonDynamicStyles in the deprecated +// Button.tsx. Matrix rules only assign CSS custom properties; the actual +// declarations live on the flat .button class so twenty-front styled(Button) +// overrides keep beating them at (0,1,0) specificity, exactly like the legacy +// single Linaria class did. +// Cascade order inside each branch: base, then [data-focus], then +// [data-disabled], then [data-disabled][data-focus] — later rules win, which +// replicates the Linaria switch precedence (disabled > focus > base). + +.wrapper { + max-width: none; + position: relative; + width: auto; +} + +.wrapperLoading { + max-width: calc(100% - var(--t-spacing-8)); +} + +.button { + --tw-button-color: var(--btn-color, var(--t-font-color-secondary)); + + align-items: center; + background: var(--btn-bg, transparent); + border-color: var(--btn-border-color, transparent); + border-radius: var(--btn-radius, var(--t-border-radius-sm)); + border-style: solid; + border-width: var(--btn-border-width, 1px 1px 1px 1px); + box-shadow: var(--btn-box-shadow, none); + box-sizing: border-box; + color: var(--btn-color, var(--t-font-color-secondary)); + cursor: pointer; + display: flex; + flex-direction: row; + font-family: var(--t-font-family); + font-size: var(--t-font-size-md); + font-weight: 500; + gap: var(--t-spacing-1); + justify-content: var(--btn-justify, flex-start); + padding: 0 var(--t-spacing-2) 0 var(--t-spacing-2); + text-decoration: none; + transition: background 0.1s ease; + white-space: nowrap; + width: auto; + + &:hover { + background: var(--btn-hover-bg, transparent); + } + + &:active { + background: var(--btn-active-bg, transparent); + } + + &:focus { + outline: none; + } + + &[data-disabled] { + cursor: not-allowed; + } +} + +.small { + height: 24px; +} + +.medium { + height: 32px; +} + +// Applied to both the wrapper and the button; declared after them so the +// width wins the cascade at equal specificity. +.fullWidth { + width: 100%; +} + +// position only reshapes radius and collapses inner borders. The legacy +// '1px 1px' override for primary/danger is omitted: it computes to the same +// 1px on every side as the default. +.button[data-position='left'] { + --btn-radius: var(--t-border-radius-sm) 0px 0px var(--t-border-radius-sm); + --btn-border-width: 1px 0px 1px 1px; +} + +.button[data-position='middle'] { + --btn-radius: 0px; + --btn-border-width: 1px 0px 1px 0px; +} + +.button[data-position='right'] { + --btn-radius: 0px var(--t-border-radius-sm) var(--t-border-radius-sm) 0px; + --btn-border-width: 1px 1px 1px 0px; +} + +// variant=primary, not inverted, keyed by accent. +// focus-needs-enabled mirrors the one asymmetry of the legacy switch: the +// default accent only shows its focus border when enabled, while blue and +// danger keep the focus border even when disabled. +$primary-accents: ( + 'default': ( + bg: var(--t-background-secondary), + bg-disabled: var(--t-background-secondary), + border: var(--t-background-transparent-light), + border-focus: var(--t-color-blue), + focus-needs-enabled: true, + shadow-focus: var(--t-accent-tertiary), + color: var(--t-font-color-secondary), + color-disabled: var(--t-font-color-extra-light), + hover: var(--t-background-tertiary), + active: var(--t-background-quaternary), + ), + 'blue': ( + bg: var(--t-color-blue), + bg-disabled: var(--t-accent-accent4060), + border: var(--t-background-transparent-light), + border-focus: var(--t-color-blue), + focus-needs-enabled: false, + shadow-focus: var(--t-accent-tertiary), + color: $gray-scale-light-gray1, + color-disabled: $gray-scale-light-gray1, + hover: var(--t-color-blue10), + active: var(--t-color-blue12), + ), + 'danger': ( + bg: var(--t-color-red), + bg-disabled: var(--t-color-red), + border: var(--t-background-transparent-light), + border-focus: var(--t-color-red), + focus-needs-enabled: false, + shadow-focus: var(--t-color-red3), + color: var(--t-background-primary), + color-disabled: var(--t-background-primary), + hover: var(--t-color-red8), + active: var(--t-color-red10), + ), +); + +@each $accent, $tokens in $primary-accents { + .button[data-variant='primary'][data-accent='#{$accent}']:not( + [data-inverted] + ) { + --btn-bg: #{map.get($tokens, bg)}; + --btn-border-color: #{map.get($tokens, border)}; + --btn-color: #{map.get($tokens, color)}; + --btn-hover-bg: #{map.get($tokens, hover)}; + --btn-active-bg: #{map.get($tokens, active)}; + } + + .button[data-variant='primary'][data-accent='#{$accent}']:not( + [data-inverted] + )[data-focus] { + --btn-border-color: #{map.get($tokens, border-focus)}; + --btn-box-shadow: 0 0 0 3px #{map.get($tokens, shadow-focus)}; + } + + .button[data-variant='primary'][data-accent='#{$accent}']:not( + [data-inverted] + )[data-disabled] { + --btn-bg: #{map.get($tokens, bg-disabled)}; + --btn-box-shadow: none; + --btn-color: #{map.get($tokens, color-disabled)}; + --btn-hover-bg: #{map.get($tokens, bg-disabled)}; + --btn-active-bg: #{map.get($tokens, bg-disabled)}; + + @if map.get($tokens, focus-needs-enabled) { + --btn-border-color: #{map.get($tokens, border)}; + } + } +} + +// variant=primary, inverted: identical across accents except for the text +// color. +$primary-inverted-colors: ( + 'default': var(--t-font-color-secondary), + 'blue': var(--t-color-blue), + 'danger': var(--t-color-red), +); + +.button[data-variant='primary'][data-inverted] { + --btn-bg: var(--t-background-primary); + --btn-border-color: var(--t-background-transparent-light); + --btn-hover-bg: var(--t-background-secondary); + --btn-active-bg: var(--t-background-tertiary); +} + +@each $accent, $color in $primary-inverted-colors { + .button[data-variant='primary'][data-accent='#{$accent}'][data-inverted] { + --btn-color: #{$color}; + } +} + +.button[data-variant='primary'][data-inverted][data-focus] { + --btn-box-shadow: 0 0 0 3px var(--t-background-transparent-medium); +} + +.button[data-variant='primary'][data-inverted][data-disabled] { + --btn-box-shadow: none; + --btn-hover-bg: var(--t-background-primary); + --btn-active-bg: var(--t-background-primary); +} + +// variant=secondary|tertiary, not inverted, keyed by accent. Background stays +// transparent (the .button fallback). The two variants only differ in their +// resting border: secondary uses border-secondary, tertiary is borderless. +// focus-needs-enabled-secondary mirrors the legacy gating: only +// secondary/default hides its focus border when disabled. +$secondary-accents: ( + 'default': ( + border-secondary: var(--t-background-transparent-medium), + border-focus: var(--t-color-blue), + focus-needs-enabled-secondary: true, + shadow-focus: var(--t-accent-tertiary), + color: var(--t-font-color-secondary), + color-disabled: var(--t-font-color-extra-light), + hover: var(--t-background-transparent-light), + active: var(--t-background-transparent-light), + ), + 'blue': ( + border-secondary: var(--t-accent-primary), + border-focus: var(--t-color-blue), + focus-needs-enabled-secondary: false, + shadow-focus: var(--t-accent-tertiary), + color: var(--t-color-blue), + color-disabled: var(--t-accent-accent4060), + hover: var(--t-accent-tertiary), + active: var(--t-accent-secondary), + ), + 'danger': ( + border-secondary: var(--t-border-color-danger), + border-focus: var(--t-color-red), + focus-needs-enabled-secondary: false, + shadow-focus: var(--t-color-red3), + color: var(--t-font-color-danger), + color-disabled: var(--t-color-red5), + hover: var(--t-background-danger), + active: var(--t-background-danger), + ), +); + +@each $accent, $tokens in $secondary-accents { + .button:is( + [data-variant='secondary'], + [data-variant='tertiary'] + )[data-accent='#{$accent}']:not([data-inverted]) { + --btn-color: #{map.get($tokens, color)}; + --btn-hover-bg: #{map.get($tokens, hover)}; + --btn-active-bg: #{map.get($tokens, active)}; + } + + .button[data-variant='secondary'][data-accent='#{$accent}']:not( + [data-inverted] + ) { + --btn-border-color: #{map.get($tokens, border-secondary)}; + } + + .button:is( + [data-variant='secondary'], + [data-variant='tertiary'] + )[data-accent='#{$accent}']:not([data-inverted])[data-focus] { + --btn-border-color: #{map.get($tokens, border-focus)}; + --btn-box-shadow: 0 0 0 3px #{map.get($tokens, shadow-focus)}; + } + + .button:is( + [data-variant='secondary'], + [data-variant='tertiary'] + )[data-accent='#{$accent}']:not([data-inverted])[data-disabled] { + --btn-box-shadow: none; + --btn-color: #{map.get($tokens, color-disabled)}; + --btn-hover-bg: transparent; + --btn-active-bg: transparent; + } + + @if map.get($tokens, focus-needs-enabled-secondary) { + .button[data-variant='secondary'][data-accent='#{$accent}']:not( + [data-inverted] + )[data-disabled] { + --btn-border-color: #{map.get($tokens, border-secondary)}; + } + } +} + +// variant=secondary|tertiary, inverted: identical across accents. Hover and +// active stay enabled even when disabled, matching the legacy source. +.button:is( + [data-variant='secondary'], + [data-variant='tertiary'] + )[data-inverted] { + --btn-color: var(--t-font-color-inverted); + --btn-hover-bg: var(--t-background-transparent-light); + --btn-active-bg: var(--t-background-transparent-medium); +} + +.button[data-variant='secondary'][data-inverted] { + --btn-border-color: var(--t-background-transparent-primary); +} + +.button:is( + [data-variant='secondary'], + [data-variant='tertiary'] + )[data-inverted][data-focus] { + --btn-border-color: #{$gray-scale-light-gray1}; + --btn-box-shadow: 0 0 0 3px var(--t-background-transparent-medium); +} + +.button:is( + [data-variant='secondary'], + [data-variant='tertiary'] + )[data-inverted][data-disabled] { + --btn-box-shadow: none; +} + +.button[data-variant='secondary'][data-inverted][data-disabled] { + --btn-border-color: #{$gray-scale-light-gray1}; +} diff --git a/packages/twenty-ui/src/input/button/components/Button/Button.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/Button/Button.module.scss.d.ts new file mode 100644 index 0000000000..3cf3ea6a31 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/Button.module.scss.d.ts @@ -0,0 +1,9 @@ +declare const classNames: { + readonly wrapper: 'wrapper'; + readonly wrapperLoading: 'wrapperLoading'; + readonly button: 'button'; + readonly small: 'small'; + readonly medium: 'medium'; + readonly fullWidth: 'fullWidth'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/Button/Button.tsx b/packages/twenty-ui/src/input/button/components/Button/Button.tsx new file mode 100644 index 0000000000..70960a2093 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/Button.tsx @@ -0,0 +1,136 @@ +import { clsx } from 'clsx'; +import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +import { type IconComponent } from '@ui/display/icon/types/IconComponent'; +import { ButtonHotkeys } from '@ui/input/button/components/Button/internal/ButtonHotKeys'; +import { ButtonIcon } from '@ui/input/button/components/Button/internal/ButtonIcon'; +import { ButtonSoon } from '@ui/input/button/components/Button/internal/ButtonSoon'; +import { useIsMobile } from '@ui/utilities'; +import { type ClickOutsideAttributes } from '@ui/utilities/types/ClickOutsideAttributes'; +import { ButtonText } from './internal/ButtonText'; + +import styles from './Button.module.scss'; + +export type ButtonSize = 'medium' | 'small'; +export type ButtonPosition = 'standalone' | 'left' | 'middle' | 'right'; +export type ButtonVariant = 'primary' | 'secondary' | 'tertiary'; +export type ButtonAccent = 'default' | 'blue' | 'danger'; + +export type ButtonProps = { + id?: string; + className?: string; + Icon?: IconComponent; + title?: string; + fullWidth?: boolean; + variant?: ButtonVariant; + inverted?: boolean; + size?: ButtonSize; + position?: ButtonPosition; + accent?: ButtonAccent; + soon?: boolean; + justify?: 'center' | 'flex-start' | 'flex-end'; + disabled?: boolean; + focus?: boolean; + onClick?: (event: React.MouseEvent) => void; + to?: string; + target?: string; + dataTestId?: string; + hotkeys?: string[]; + ariaLabel?: string; + isLoading?: boolean; +} & Pick, 'type'> & + ClickOutsideAttributes; + +export const Button = ({ + className, + Icon, + title, + id, + fullWidth = false, + variant = 'primary', + inverted = false, + size = 'medium', + accent = 'default', + position = 'standalone', + soon = false, + disabled = false, + justify = 'flex-start', + focus: propFocus = false, + onClick, + to, + target, + dataTestId, + dataClickOutsideId, + dataGloballyPreventClickOutside, + hotkeys, + ariaLabel, + type, + isLoading = false, +}: ButtonProps) => { + const isMobile = useIsMobile(); + + const [isFocused, setIsFocused] = useState(propFocus); + const isDisabled = soon || disabled; + + // Replaces the legacy Linaria `as` polymorphism: react-router Link when a + // `to` is provided, a native button otherwise. Typed as any to forward all + // props untyped, exactly like the legacy `as` prop did. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ButtonComponent: any = to ? Link : 'button'; + + return ( +
+ setIsFocused(true)} + onBlur={() => setIsFocused(false)} + style={{ '--btn-justify': justify } as React.CSSProperties} + > + {(isLoading || Icon) && ( + + )} + {isDefined(title) && ( + + )} + {hotkeys && !isMobile && ( + + )} + {soon && } + +
+ ); +}; diff --git a/packages/twenty-ui/src/input/button/components/Button/constant.ts b/packages/twenty-ui/src/input/button/components/Button/constant.ts new file mode 100644 index 0000000000..62b9b9e064 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/constant.ts @@ -0,0 +1 @@ +export const baseTransitionTiming = 300; diff --git a/packages/twenty-ui/src/input/button/components/Button/internal/ButtonHotKeys.module.scss b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonHotKeys.module.scss new file mode 100644 index 0000000000..c7b46a754d --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonHotKeys.module.scss @@ -0,0 +1,35 @@ +.separator { + background: var(--t-font-color-light); + height: var(--t-spacing-4); + margin: 0; + width: 1px; + + &[data-size='small'] { + height: var(--t-spacing-2); + } + + &[data-accent='blue'] { + background: var(--t-buttons-secondary-text-color); + } + + &[data-accent='danger'] { + background: var(--t-border-color-danger); + } +} + +.shortcutLabel { + color: var(--t-font-color-light); + font-weight: var(--t-font-weight-medium); + + &[data-accent='blue'] { + color: var(--t-buttons-secondary-text-color); + } + + &[data-accent='danger'] { + color: var(--t-color-red8); + } + + &[data-accent='danger'][data-variant='primary'] { + color: var(--t-border-color-danger); + } +} diff --git a/packages/twenty-ui/src/input/button/components/Button/internal/ButtonHotKeys.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonHotKeys.module.scss.d.ts new file mode 100644 index 0000000000..f8d905ac62 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonHotKeys.module.scss.d.ts @@ -0,0 +1,5 @@ +declare const classNames: { + readonly separator: 'separator'; + readonly shortcutLabel: 'shortcutLabel'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/Button/internal/ButtonHotKeys.tsx b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonHotKeys.tsx new file mode 100644 index 0000000000..07ab119c99 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonHotKeys.tsx @@ -0,0 +1,33 @@ +import { + type ButtonAccent, + type ButtonSize, + type ButtonVariant, +} from '@ui/input/button/components/Button/Button'; +import { getOsShortcutSeparator } from '@ui/utilities'; + +import styles from './ButtonHotKeys.module.scss'; + +export const ButtonHotkeys = ({ + size, + accent, + variant, + hotkeys, +}: { + size: ButtonSize; + accent: ButtonAccent; + variant: ButtonVariant; + hotkeys: string[]; +}) => { + return ( + <> +
+
+ {hotkeys.join(getOsShortcutSeparator())} +
+ + ); +}; diff --git a/packages/twenty-ui/src/input/button/components/Button/internal/ButtonIcon.module.scss b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonIcon.module.scss new file mode 100644 index 0000000000..475e5f8a46 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonIcon.module.scss @@ -0,0 +1,32 @@ +// Mirrors baseTransitionTiming (300) in ../constant.ts — keep in sync. +$base-transition-timing: 300ms; + +.iconWrapper { + align-items: center; + display: flex; + pointer-events: none; +} + +.icon { + align-items: center; + color: var(--tw-button-color); + display: flex; + height: calc(100% - var(--t-spacing-4)); + opacity: 1; + transition: opacity ($base-transition-timing * 0.5) ease; + transition-delay: $base-transition-timing * 0.5; + + &[data-loading] { + opacity: 0; + transition-delay: 0ms; + } +} + +.loader { + left: var(--t-spacing-2); + opacity: 1; + position: absolute; + transition: opacity ($base-transition-timing * 0.5) ease; + transition-delay: $base-transition-timing * 0.5; + width: var(--t-spacing-6); +} diff --git a/packages/twenty-ui/src/input/button/components/Button/internal/ButtonIcon.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonIcon.module.scss.d.ts new file mode 100644 index 0000000000..acd6652bf4 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonIcon.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly iconWrapper: 'iconWrapper'; + readonly icon: 'icon'; + readonly loader: 'loader'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/Button/internal/ButtonIcon.tsx b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonIcon.tsx new file mode 100644 index 0000000000..8d8f5fb1f9 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonIcon.tsx @@ -0,0 +1,31 @@ +import { type IconComponent } from '@ui/display'; +import { Loader } from '@ui/feedback'; +import { ThemeContext } from '@ui/theme-constants'; +import { useContext } from 'react'; + +import styles from './ButtonIcon.module.scss'; + +export const ButtonIcon = ({ + Icon, + isLoading, +}: { + Icon?: IconComponent; + isLoading?: boolean; +}) => { + const { theme } = useContext(ThemeContext); + + return ( +
+ {isLoading && ( +
+ +
+ )} + {Icon && ( +
+ +
+ )} +
+ ); +}; diff --git a/packages/twenty-ui/src/input/button/components/Button/internal/ButtonSoon.module.scss b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonSoon.module.scss new file mode 100644 index 0000000000..57cd7cbad8 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonSoon.module.scss @@ -0,0 +1,4 @@ +.soonPillContainer { + display: flex; + margin-left: auto; +} diff --git a/packages/twenty-ui/src/input/button/components/Button/internal/ButtonSoon.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonSoon.module.scss.d.ts new file mode 100644 index 0000000000..6f168cd4be --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonSoon.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly soonPillContainer: 'soonPillContainer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/Button/internal/ButtonSoon.tsx b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonSoon.tsx new file mode 100644 index 0000000000..50abfde4e0 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonSoon.tsx @@ -0,0 +1,13 @@ +import { Pill } from '@ui/components'; + +import styles from './ButtonSoon.module.scss'; + +type ButtonSoonProps = { + label?: string; +}; + +export const ButtonSoon = ({ label = 'Soon' }: ButtonSoonProps) => ( + + + +); diff --git a/packages/twenty-ui/src/input/button/components/Button/internal/ButtonText.module.scss b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonText.module.scss new file mode 100644 index 0000000000..7cb0c4abf8 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonText.module.scss @@ -0,0 +1,44 @@ +// Mirrors baseTransitionTiming (300) in ../constant.ts — keep in sync. +$base-transition-timing: 300ms; + +.textWrapper { + align-items: center; + display: flex; + height: 100%; + justify-content: center; + position: relative; +} + +.text { + clip-path: inset(0 0 0 0); + overflow: hidden; + transform: none; + transition: + transform $base-transition-timing ease, + clip-path $base-transition-timing ease; + transition-delay: $base-transition-timing * 0.25; + white-space: nowrap; + + &[data-loading] { + clip-path: inset(0 var(--t-spacing-12) 0 0); + transform: translateX(var(--t-spacing-7)); + transition-delay: 0ms; + } + + &[data-loading][data-has-icon] { + clip-path: inset(0 var(--t-spacing-6) 0 0); + transform: translateX(var(--t-spacing-3)); + } +} + +.ellipsis { + clip-path: inset(0 0 0 var(--t-spacing-6)); + overflow: hidden; + position: absolute; + right: 0; + transition: clip-path $base-transition-timing ease; + + &[data-loading] { + clip-path: inset(0 0 0 0); + } +} diff --git a/packages/twenty-ui/src/input/button/components/Button/internal/ButtonText.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonText.module.scss.d.ts new file mode 100644 index 0000000000..17cb79c585 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonText.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly textWrapper: 'textWrapper'; + readonly text: 'text'; + readonly ellipsis: 'ellipsis'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/Button/internal/ButtonText.tsx b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonText.tsx new file mode 100644 index 0000000000..145d0093c9 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/Button/internal/ButtonText.tsx @@ -0,0 +1,26 @@ +import styles from './ButtonText.module.scss'; + +export const ButtonText = ({ + hasIcon = false, + isLoading, + title, +}: { + isLoading?: boolean; + hasIcon: boolean; + title?: string; +}) => { + return ( +
+
+ {title} +
+
+ ... +
+
+ ); +}; diff --git a/packages/twenty-ui/src/input/button/components/ButtonGroup.module.scss b/packages/twenty-ui/src/input/button/components/ButtonGroup.module.scss new file mode 100644 index 0000000000..f93d20c4ba --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/ButtonGroup.module.scss @@ -0,0 +1,4 @@ +.container { + border-radius: var(--t-border-radius-md); + display: flex; +} diff --git a/packages/twenty-ui/src/input/button/components/ButtonGroup.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/ButtonGroup.module.scss.d.ts new file mode 100644 index 0000000000..8df71b4dea --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/ButtonGroup.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly container: 'container'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/ButtonGroup.tsx b/packages/twenty-ui/src/input/button/components/ButtonGroup.tsx new file mode 100644 index 0000000000..d612400206 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/ButtonGroup.tsx @@ -0,0 +1,57 @@ +import { clsx } from 'clsx'; +import React, { type ReactNode } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +import { type ButtonPosition, type ButtonProps } from './Button/Button'; + +import styles from './ButtonGroup.module.scss'; + +export type ButtonGroupProps = Partial< + Pick +> & { + className?: string; + children: ReactNode[]; +}; + +export const ButtonGroup = ({ + className, + children, + variant, + size, + accent, +}: ButtonGroupProps) => { + return ( +
+ {React.Children.map(children, (child, index) => { + if (!React.isValidElement(child)) return null; + + let position: ButtonPosition; + + if (index === 0) { + position = 'left'; + } else if (index === children.length - 1) { + position = 'right'; + } else { + position = 'middle'; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const additionalProps: any = { position, variant, accent, size }; + + if (isDefined(variant)) { + additionalProps.variant = variant; + } + + if (isDefined(accent)) { + additionalProps.accent = accent; + } + + if (isDefined(size)) { + additionalProps.size = size; + } + + return React.cloneElement(child, additionalProps); + })} +
+ ); +}; diff --git a/packages/twenty-ui/src/input/button/components/ColorPickerButton.module.scss b/packages/twenty-ui/src/input/button/components/ColorPickerButton.module.scss new file mode 100644 index 0000000000..96b6e0849c --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/ColorPickerButton.module.scss @@ -0,0 +1,15 @@ +// The :hover rule is kept separate on purpose: at (0,1,2) it out-specifies +// the inner LightIconButton's own hover background at (0,1,1) regardless of +// stylesheet order, exactly like the legacy Linaria wrapper did. + +.wrapper button { + background-color: var(--color-picker-button-bg, transparent); +} + +.wrapper button:hover { + background-color: var(--color-picker-button-bg, transparent); +} + +.wrapper[data-selected] { + --color-picker-button-bg: var(--t-background-transparent-medium); +} diff --git a/packages/twenty-ui/src/input/button/components/ColorPickerButton.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/ColorPickerButton.module.scss.d.ts new file mode 100644 index 0000000000..6f9e27f1e2 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/ColorPickerButton.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly wrapper: 'wrapper'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/ColorPickerButton.tsx b/packages/twenty-ui/src/input/button/components/ColorPickerButton.tsx new file mode 100644 index 0000000000..842e8977f7 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/ColorPickerButton.tsx @@ -0,0 +1,28 @@ +import { ColorSample, type ColorSampleProps } from '@ui/display'; +import { + LightIconButton, + type LightIconButtonProps, +} from '@ui/input/button/components/LightIconButton'; + +import styles from './ColorPickerButton.module.scss'; + +type ColorPickerButtonProps = Pick & + Pick & { + isSelected?: boolean; + }; + +export const ColorPickerButton = ({ + colorName, + isSelected, + onClick, +}: ColorPickerButtonProps) => { + return ( +
+ } + onClick={onClick} + /> +
+ ); +}; diff --git a/packages/twenty-ui/src/input/button/components/FloatingButton.module.scss b/packages/twenty-ui/src/input/button/components/FloatingButton.module.scss new file mode 100644 index 0000000000..421f58fbe6 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/FloatingButton.module.scss @@ -0,0 +1,102 @@ +// State rules below only assign CSS custom properties; the actual +// declarations live on the flat .button class so twenty-front +// styled(FloatingButton) overrides keep beating them at (0,1,0) specificity, +// exactly like the legacy single Linaria class did. + +.button { + align-items: center; + backdrop-filter: var(--floating-button-backdrop-filter, none); + background: var(--t-background-primary); + border: var(--floating-button-border, none); + border-radius: var(--floating-button-radius, var(--t-border-radius-sm)); + box-shadow: var(--floating-button-box-shadow, none); + color: var(--floating-button-color, var(--t-font-color-secondary)); + cursor: pointer; + display: flex; + flex-direction: row; + font-family: var(--t-font-family); + font-weight: var(--t-font-weight-regular); + gap: var(--t-spacing-1); + padding: 0 var(--t-spacing-2); + text-decoration: none; + transition: background 0.1s ease; + white-space: nowrap; + + &:hover { + background: var( + --floating-button-hover-bg, + var(--t-background-transparent-lighter) + ); + } + + &:active { + background: var( + --floating-button-active-bg, + var(--t-background-transparent-medium) + ); + } + + &:focus { + outline: none; + } + + &[data-disabled] { + cursor: not-allowed; + } +} + +.small { + height: 24px; +} + +.medium { + height: 32px; +} + +// position only reshapes the radius; standalone keeps the .button default. +.button[data-position='left'] { + --floating-button-radius: var(--t-border-radius-sm) 0px 0px + var(--t-border-radius-sm); +} + +.button[data-position='middle'] { + --floating-button-radius: 0px; +} + +.button[data-position='right'] { + --floating-button-radius: 0px var(--t-border-radius-sm) + var(--t-border-radius-sm) 0px; +} + +.button[data-apply-blur] { + --floating-button-backdrop-filter: blur(20px); +} + +// box-shadow matrix from the legacy nested ternary: shadow, focus, and +// shadow+focus — the combined rule comes last so it wins the cascade. +.button[data-apply-shadow] { + --floating-button-box-shadow: + 0px 2px 4px 0px var(--t-background-transparent-light), + 0px 0px 4px 0px var(--t-background-transparent-medium); +} + +// focus is gated by !disabled in the component, like the legacy call-site. +.button[data-focus] { + --floating-button-border: 1px solid var(--t-color-blue); + --floating-button-box-shadow: 0 0 0 3px var(--t-color-blue3); + --floating-button-color: var(--t-color-blue); +} + +.button[data-apply-shadow][data-focus] { + --floating-button-box-shadow: + 0px 2px 4px 0px var(--t-background-transparent-light), + 0px 0px 4px 0px var(--t-background-transparent-medium), + 0 0 0 3px var(--t-color-blue3); +} + +// disabled comes last: it beats focus in the legacy color ternary. +.button[data-disabled] { + --floating-button-color: var(--t-font-color-extra-light); + --floating-button-hover-bg: transparent; + --floating-button-active-bg: transparent; +} diff --git a/packages/twenty-ui/src/input/button/components/FloatingButton.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/FloatingButton.module.scss.d.ts new file mode 100644 index 0000000000..de85059fc8 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/FloatingButton.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly button: 'button'; + readonly small: 'small'; + readonly medium: 'medium'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/FloatingButton.tsx b/packages/twenty-ui/src/input/button/components/FloatingButton.tsx new file mode 100644 index 0000000000..a3970b3563 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/FloatingButton.tsx @@ -0,0 +1,61 @@ +import { clsx } from 'clsx'; +import { useContext } from 'react'; +import { Link } from 'react-router-dom'; + +import { type IconComponent } from '@ui/display'; +import { ThemeContext } from '@ui/theme-constants'; + +import styles from './FloatingButton.module.scss'; + +export type FloatingButtonSize = 'small' | 'medium'; +export type FloatingButtonPosition = 'standalone' | 'left' | 'middle' | 'right'; + +export type FloatingButtonProps = { + className?: string; + Icon?: IconComponent; + title?: string; + size?: FloatingButtonSize; + position?: FloatingButtonPosition; + applyShadow?: boolean; + applyBlur?: boolean; + disabled?: boolean; + focus?: boolean; + to?: string; +}; + +export const FloatingButton = ({ + className, + Icon, + title, + size = 'small', + position = 'standalone', + applyBlur = true, + applyShadow = true, + disabled = false, + focus = false, + to, +}: FloatingButtonProps) => { + const { theme } = useContext(ThemeContext); + + // Replaces the legacy Linaria `as` polymorphism: react-router Link when a + // `to` is provided, a native button otherwise. Typed as any to forward all + // props untyped, exactly like the legacy `as` prop did. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ButtonComponent: any = to ? Link : 'button'; + + return ( + + {Icon && } + {title} + + ); +}; diff --git a/packages/twenty-ui/src/input/button/components/FloatingButtonGroup.module.scss b/packages/twenty-ui/src/input/button/components/FloatingButtonGroup.module.scss new file mode 100644 index 0000000000..b39194ee88 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/FloatingButtonGroup.module.scss @@ -0,0 +1,8 @@ +.container { + backdrop-filter: blur(20px); + border-radius: var(--t-border-radius-md); + box-shadow: + 0px 2px 4px 0px var(--t-background-transparent-light), + 0px 0px 4px 0px var(--t-background-transparent-medium); + display: inline-flex; +} diff --git a/packages/twenty-ui/src/input/button/components/FloatingButtonGroup.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/FloatingButtonGroup.module.scss.d.ts new file mode 100644 index 0000000000..8df71b4dea --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/FloatingButtonGroup.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly container: 'container'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/FloatingButtonGroup.tsx b/packages/twenty-ui/src/input/button/components/FloatingButtonGroup.tsx new file mode 100644 index 0000000000..74d48ef76e --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/FloatingButtonGroup.tsx @@ -0,0 +1,51 @@ +import { clsx } from 'clsx'; +import React from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +import { + type FloatingButtonPosition, + type FloatingButtonProps, +} from './FloatingButton'; + +import styles from './FloatingButtonGroup.module.scss'; + +export type FloatingButtonGroupProps = Pick & { + children: React.ReactElement[]; + className?: string; +}; + +export const FloatingButtonGroup = ({ + children, + size, + className, +}: FloatingButtonGroupProps) => { + return ( +
+ {React.Children.map(children, (child, index) => { + let position: FloatingButtonPosition; + + if (index === 0) { + position = 'left'; + } else if (index === children.length - 1) { + position = 'right'; + } else { + position = 'middle'; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const additionalProps: any = { + position, + size, + applyShadow: false, + applyBlur: false, + }; + + if (isDefined(size)) { + additionalProps.size = size; + } + + return React.cloneElement(child, additionalProps); + })} +
+ ); +}; diff --git a/packages/twenty-ui/src/input/button/components/FloatingIconButton.module.scss b/packages/twenty-ui/src/input/button/components/FloatingIconButton.module.scss new file mode 100644 index 0000000000..db0c12e6e3 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/FloatingIconButton.module.scss @@ -0,0 +1,109 @@ +// Ported from the legacy Linaria FloatingIconButton. State rules below are +// kept flat on .button so twenty-front styled(FloatingIconButton) overrides +// keep beating them at (0,1,0) specificity. Source order replicates the +// legacy ternary precedence: hover/active beat the isActive background, +// applyShadow beats the focus ring, and disabled wins last. + +.button { + align-items: center; + backdrop-filter: none; + background: var(--t-background-primary); + border: 1px solid var(--t-border-color-strong); + border-radius: var(--floating-icon-button-radius, var(--t-border-radius-sm)); + box-shadow: none; + box-sizing: border-box; + color: var(--t-font-color-tertiary); + cursor: pointer; + display: flex; + flex-direction: row; + font-family: var(--t-font-family); + font-weight: var(--t-font-weight-regular); + gap: var(--t-spacing-1); + justify-content: center; + padding: 0; + position: relative; + transition: background calc(var(--t-animation-duration-instant) * 1s) ease; + white-space: nowrap; +} + +.small { + height: 24px; + width: 24px; +} + +.medium { + height: 32px; + width: 32px; +} + +// Grouped buttons (any non-standalone position) are 4px smaller. +.small:not([data-position='standalone']) { + height: 20px; + width: 20px; +} + +.medium:not([data-position='standalone']) { + height: 28px; + width: 28px; +} + +.button[data-position='left'] { + --floating-icon-button-radius: var(--t-border-radius-sm) 0px 0px + var(--t-border-radius-sm); +} + +.button[data-position='middle'] { + --floating-icon-button-radius: 0px; +} + +.button[data-position='right'] { + --floating-icon-button-radius: 0px var(--t-border-radius-sm) + var(--t-border-radius-sm) 0px; +} + +.button[data-apply-blur] { + backdrop-filter: var(--t-blur-medium); +} + +.button[data-is-active] { + background: var(--t-background-transparent-medium); +} + +// Declared after [data-is-active] so hover/active win over it, like the +// legacy &:hover/&:active beating the base background. +.button:hover { + background: var(--t-background-transparent-lighter); +} + +.button:active { + background: var(--t-background-transparent-medium); +} + +.button:focus { + outline: none; +} + +// data-focus is gated by !disabled in the component, like the legacy +// call-site passing focus={focus && !disabled}. +.button[data-focus] { + border-color: var(--t-color-blue); + box-shadow: 0 0 0 3px var(--t-color-blue3); + color: var(--t-color-blue); +} + +// applyShadow wins over the focus ring, replicating the legacy ternary order. +.button[data-apply-shadow] { + box-shadow: var(--t-box-shadow-light); +} + +.button[data-disabled] { + color: var(--t-font-color-extra-light); + cursor: not-allowed; +} + +// A disabled button still matches :hover/:active; the legacy styles +// explicitly turned the background transparent there. +.button[data-disabled]:hover, +.button[data-disabled]:active { + background: transparent; +} diff --git a/packages/twenty-ui/src/input/button/components/FloatingIconButton.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/FloatingIconButton.module.scss.d.ts new file mode 100644 index 0000000000..de85059fc8 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/FloatingIconButton.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly button: 'button'; + readonly small: 'small'; + readonly medium: 'medium'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/FloatingIconButton.tsx b/packages/twenty-ui/src/input/button/components/FloatingIconButton.tsx new file mode 100644 index 0000000000..1a9a816f87 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/FloatingIconButton.tsx @@ -0,0 +1,58 @@ +import { clsx } from 'clsx'; +import React, { useContext } from 'react'; + +import { type IconComponent } from '@ui/display'; +import { ThemeContext } from '@ui/theme-constants'; + +import styles from './FloatingIconButton.module.scss'; + +export type FloatingIconButtonSize = 'small' | 'medium'; +export type FloatingIconButtonPosition = + | 'standalone' + | 'left' + | 'middle' + | 'right'; + +export type FloatingIconButtonProps = { + className?: string; + Icon?: IconComponent; + size?: FloatingIconButtonSize; + position?: FloatingIconButtonPosition; + applyShadow?: boolean; + applyBlur?: boolean; + disabled?: boolean; + focus?: boolean; + onClick?: (event: React.MouseEvent) => void; + isActive?: boolean; +}; + +export const FloatingIconButton = ({ + className, + Icon, + size = 'small', + position = 'standalone', + applyShadow = true, + applyBlur = true, + disabled = false, + focus = false, + onClick, + isActive, +}: FloatingIconButtonProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/input/button/components/FloatingIconButtonGroup.module.scss b/packages/twenty-ui/src/input/button/components/FloatingIconButtonGroup.module.scss new file mode 100644 index 0000000000..b19103a8c6 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/FloatingIconButtonGroup.module.scss @@ -0,0 +1,11 @@ +.container { + backdrop-filter: blur(20px); + background-color: var(--t-background-primary); + border-radius: var(--t-border-radius-sm); + box-shadow: + 0px 2px 4px 0px var(--t-background-transparent-light), + 0px 0px 4px 0px var(--t-background-transparent-medium); + display: inline-flex; + gap: 2px; + padding: 2px; +} diff --git a/packages/twenty-ui/src/input/button/components/FloatingIconButtonGroup.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/FloatingIconButtonGroup.module.scss.d.ts new file mode 100644 index 0000000000..8df71b4dea --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/FloatingIconButtonGroup.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly container: 'container'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/FloatingIconButtonGroup.tsx b/packages/twenty-ui/src/input/button/components/FloatingIconButtonGroup.tsx new file mode 100644 index 0000000000..05f0c24a5d --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/FloatingIconButtonGroup.tsx @@ -0,0 +1,58 @@ +import { clsx } from 'clsx'; +import { type MouseEvent } from 'react'; + +import { type IconComponent } from '@ui/display'; + +import { + FloatingIconButton, + type FloatingIconButtonPosition, + type FloatingIconButtonProps, +} from './FloatingIconButton'; + +import styles from './FloatingIconButtonGroup.module.scss'; + +export type FloatingIconButtonGroupProps = Pick< + FloatingIconButtonProps, + 'className' | 'size' +> & { + iconButtons: { + Icon: IconComponent; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onClick?: (event: MouseEvent) => void; + isActive?: boolean; + }[]; +}; + +export const FloatingIconButtonGroup = ({ + iconButtons, + size, + className, +}: FloatingIconButtonGroupProps) => { + return ( +
+ {iconButtons.map(({ Icon, onClick, isActive }, index) => { + const position: FloatingIconButtonPosition = + iconButtons.length === 1 + ? 'standalone' + : index === 0 + ? 'left' + : index === iconButtons.length - 1 + ? 'right' + : 'middle'; + + return ( + + ); + })} +
+ ); +}; diff --git a/packages/twenty-ui/src/input/button/components/IconButton.module.scss b/packages/twenty-ui/src/input/button/components/IconButton.module.scss new file mode 100644 index 0000000000..8fab7c1f26 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/IconButton.module.scss @@ -0,0 +1,307 @@ +@use 'sass:map'; + +// The variant × accent × disabled × focus matrix below is ported +// token-for-token from computeIconButtonDynamicStyles in the deprecated +// IconButton.tsx. Matrix rules only assign CSS custom properties; the actual +// declarations live on the flat .button class so twenty-front +// styled(IconButton) overrides keep beating them at (0,1,0) specificity, +// exactly like the legacy single Linaria class did. +// Cascade order inside each branch: base, then [data-focus], then +// [data-disabled] — later rules win, which replicates the legacy switch +// precedence (disabled > focus > base). + +.button { + align-items: center; + background: var(--icon-button-bg, transparent); + border-color: var(--icon-button-border-color, transparent); + border-radius: var(--icon-button-radius, var(--t-border-radius-sm)); + border-style: solid; + border-width: var(--icon-button-border-width, 1px); + box-shadow: var(--icon-button-box-shadow, none); + box-sizing: border-box; + color: var(--icon-button-color, var(--t-font-color-secondary)); + cursor: pointer; + display: flex; + flex-direction: row; + font-family: var(--t-font-family); + font-weight: 500; + gap: var(--t-spacing-1); + justify-content: center; + opacity: var(--icon-button-opacity, 1); + padding: 0; + transition: background 0.1s ease; + white-space: nowrap; + + &:hover { + background: var(--icon-button-hover-bg, transparent); + } + + &:active { + background: var(--icon-button-active-bg, transparent); + } + + &:focus { + outline: none; + } + + &[data-disabled] { + cursor: not-allowed; + } +} + +.small { + height: 24px; + min-width: 24px; +} + +.medium { + height: 32px; + min-width: 32px; +} + +// position only reshapes the border radius; standalone keeps the .button +// fallback radius. +.button[data-position='left'] { + --icon-button-radius: var(--t-border-radius-sm) 0px 0px + var(--t-border-radius-sm); +} + +.button[data-position='middle'] { + --icon-button-radius: 0px; +} + +.button[data-position='right'] { + --icon-button-radius: 0px var(--t-border-radius-sm) var(--t-border-radius-sm) + 0px; +} + +// Default border widths: tertiary is borderless and middle collapses the +// horizontal borders for primary/secondary; the .button fallback covers the +// remaining 1px cases. +.button[data-variant='tertiary'] { + --icon-button-border-width: 0; +} + +.button[data-position='middle']:is( + [data-variant='primary'], + [data-variant='secondary'] + ) { + --icon-button-border-width: 1px 0px; +} + +// focusOverride from the legacy source: an enabled focused button always +// shows a full 1px border, even for tertiary (borderless) and middle +// (collapsed) buttons. +.button[data-focus]:not([data-disabled]) { + --icon-button-border-width: 1px 1px; +} + +// variant=primary, keyed by accent. Null entries mean the disabled state +// keeps the base/focus value, mirroring the legacy switch. +$primary-accents: ( + 'default': ( + bg: var(--t-background-secondary), + border: var(--t-background-transparent-light), + border-focus: var(--t-color-blue), + border-disabled: null, + shadow-focus: var(--t-accent-tertiary), + color: var(--t-font-color-secondary), + color-disabled: var(--t-font-color-extra-light), + opacity-disabled: null, + hover: var(--t-background-tertiary), + active: var(--t-background-quaternary), + hover-disabled: var(--t-background-secondary), + active-disabled: var(--t-background-secondary), + ), + 'blue': ( + bg: var(--t-color-blue), + border: var(--t-background-transparent-light), + border-focus: var(--t-color-blue), + border-disabled: transparent, + shadow-focus: var(--t-accent-tertiary), + color: var(--t-gray-scale-gray1), + color-disabled: null, + opacity-disabled: 0.24, + hover: var(--t-color-blue10), + active: var(--t-color-blue12), + hover-disabled: var(--t-color-blue), + active-disabled: var(--t-color-blue), + ), + 'danger': ( + bg: var(--t-color-red), + border: var(--t-background-transparent-light), + border-focus: var(--t-color-red), + border-disabled: transparent, + shadow-focus: var(--t-color-red3), + color: var(--t-gray-scale-gray1), + color-disabled: null, + opacity-disabled: 0.24, + hover: var(--t-color-red10), + active: var(--t-color-red10), + hover-disabled: var(--t-color-red), + active-disabled: var(--t-color-red), + ), +); + +@each $accent, $tokens in $primary-accents { + .button[data-variant='primary'][data-accent='#{$accent}'] { + --icon-button-bg: #{map.get($tokens, bg)}; + --icon-button-border-color: #{map.get($tokens, border)}; + --icon-button-color: #{map.get($tokens, color)}; + --icon-button-hover-bg: #{map.get($tokens, hover)}; + --icon-button-active-bg: #{map.get($tokens, active)}; + } + + // borderColor follows the raw focus flag in the legacy source, so + // disabled+focus keeps it unless the accent defines border-disabled. + .button[data-variant='primary'][data-accent='#{$accent}'][data-focus] { + --icon-button-border-color: #{map.get($tokens, border-focus)}; + --icon-button-box-shadow: 0 0 0 3px #{map.get($tokens, shadow-focus)}; + } + + .button[data-variant='primary'][data-accent='#{$accent}'][data-disabled] { + --icon-button-box-shadow: none; + --icon-button-hover-bg: #{map.get($tokens, hover-disabled)}; + --icon-button-active-bg: #{map.get($tokens, active-disabled)}; + + @if map.get($tokens, border-disabled) { + --icon-button-border-color: #{map.get($tokens, border-disabled)}; + } + + @if map.get($tokens, color-disabled) { + --icon-button-color: #{map.get($tokens, color-disabled)}; + } + + @if map.get($tokens, opacity-disabled) { + --icon-button-opacity: #{map.get($tokens, opacity-disabled)}; + } + } +} + +// variant=secondary|tertiary. The two variants share backgrounds and disabled +// treatment; they differ in their resting border (secondary is outlined, +// tertiary borderless) and in how that border reacts to focus, so the border +// logic is spelled out per accent instead of mapped. + +// accent=default +.button:is( + [data-variant='secondary'], + [data-variant='tertiary'] + )[data-accent='default'] { + --icon-button-hover-bg: var(--t-background-transparent-light); + --icon-button-active-bg: var(--t-background-transparent-light); +} + +.button[data-variant='secondary'][data-accent='default'] { + --icon-button-border-color: var(--t-background-transparent-medium); + --icon-button-color: var(--t-font-color-secondary); +} + +.button[data-variant='tertiary'][data-accent='default'] { + --icon-button-color: var(--t-font-color-tertiary); +} + +// background follows the raw focus flag (it stays on for disabled+focus). +.button:is( + [data-variant='secondary'], + [data-variant='tertiary'] + )[data-accent='default'][data-focus] { + --icon-button-bg: var(--t-background-transparent-primary); + --icon-button-border-color: var(--t-color-blue); + --icon-button-box-shadow: 0 0 0 3px var(--t-accent-tertiary); +} + +.button:is( + [data-variant='secondary'], + [data-variant='tertiary'] + )[data-accent='default'][data-disabled] { + --icon-button-box-shadow: none; + --icon-button-color: var(--t-font-color-extra-light); + --icon-button-hover-bg: transparent; + --icon-button-active-bg: transparent; +} + +// The secondary border only turns blue on enabled focus; tertiary keeps the +// blue border even for disabled+focus (raw focus in the legacy source). +.button[data-variant='secondary'][data-accent='default'][data-disabled] { + --icon-button-border-color: var(--t-background-transparent-medium); +} + +// accent=blue +.button:is( + [data-variant='secondary'], + [data-variant='tertiary'] + )[data-accent='blue'] { + --icon-button-color: var(--t-color-blue); + --icon-button-hover-bg: var(--t-accent-tertiary); + --icon-button-active-bg: var(--t-accent-secondary); +} + +.button[data-variant='secondary'][data-accent='blue'] { + --icon-button-border-color: var(--t-color-blue); +} + +.button:is( + [data-variant='secondary'], + [data-variant='tertiary'] + )[data-accent='blue'][data-focus] { + --icon-button-bg: var(--t-background-transparent-primary); + --icon-button-box-shadow: 0 0 0 3px var(--t-accent-tertiary); +} + +// Only the tertiary border reacts to focus for the blue accent; the +// secondary border is keyed on disabled alone. +.button[data-variant='tertiary'][data-accent='blue'][data-focus] { + --icon-button-border-color: var(--t-color-blue); +} + +.button:is( + [data-variant='secondary'], + [data-variant='tertiary'] + )[data-accent='blue'][data-disabled] { + --icon-button-box-shadow: none; + --icon-button-color: var(--t-accent-accent4060); + --icon-button-hover-bg: transparent; + --icon-button-active-bg: transparent; +} + +.button[data-variant='secondary'][data-accent='blue'][data-disabled] { + --icon-button-border-color: var(--t-color-blue5); +} + +// accent=danger +.button:is( + [data-variant='secondary'], + [data-variant='tertiary'] + )[data-accent='danger'] { + --icon-button-color: var(--t-font-color-danger); + --icon-button-hover-bg: var(--t-background-danger); + --icon-button-active-bg: var(--t-background-danger); +} + +.button[data-variant='secondary'][data-accent='danger'] { + --icon-button-border-color: var(--t-border-color-danger); +} + +// danger keeps a transparent background even on focus; only the shadow and +// the tertiary border react. +.button:is( + [data-variant='secondary'], + [data-variant='tertiary'] + )[data-accent='danger'][data-focus] { + --icon-button-box-shadow: 0 0 0 3px var(--t-color-red3); +} + +.button[data-variant='tertiary'][data-accent='danger'][data-focus] { + --icon-button-border-color: var(--t-color-red); +} + +.button:is( + [data-variant='secondary'], + [data-variant='tertiary'] + )[data-accent='danger'][data-disabled] { + --icon-button-box-shadow: none; + --icon-button-color: var(--t-color-red5); + --icon-button-hover-bg: transparent; + --icon-button-active-bg: transparent; +} diff --git a/packages/twenty-ui/src/input/button/components/IconButton.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/IconButton.module.scss.d.ts new file mode 100644 index 0000000000..de85059fc8 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/IconButton.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly button: 'button'; + readonly small: 'small'; + readonly medium: 'medium'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/IconButton.tsx b/packages/twenty-ui/src/input/button/components/IconButton.tsx new file mode 100644 index 0000000000..bcf985b3d9 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/IconButton.tsx @@ -0,0 +1,64 @@ +import { clsx } from 'clsx'; +import React, { useContext } from 'react'; + +import { type IconComponent } from '@ui/display'; +import { ThemeContext } from '@ui/theme-constants'; + +import styles from './IconButton.module.scss'; + +export type IconButtonSize = 'medium' | 'small'; +export type IconButtonPosition = 'standalone' | 'left' | 'middle' | 'right'; +export type IconButtonVariant = 'primary' | 'secondary' | 'tertiary'; +export type IconButtonAccent = 'default' | 'blue' | 'danger'; + +export type IconButtonProps = { + className?: string; + Icon?: IconComponent; + variant?: IconButtonVariant; + size?: IconButtonSize; + position?: IconButtonPosition; + accent?: IconButtonAccent; + disabled?: boolean; + focus?: boolean; + dataTestId?: string; + ariaLabel?: string; + onClick?: (event: React.MouseEvent) => void; + to?: string; +}; + +export const IconButton = ({ + className, + Icon, + variant = 'primary', + size = 'medium', + accent = 'default', + position = 'standalone', + disabled = false, + focus = false, + dataTestId, + ariaLabel, + onClick, + to, +}: IconButtonProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/input/button/components/IconButtonGroup.module.scss b/packages/twenty-ui/src/input/button/components/IconButtonGroup.module.scss new file mode 100644 index 0000000000..6351090f93 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/IconButtonGroup.module.scss @@ -0,0 +1,18 @@ +.container { + align-items: flex-start; + backdrop-filter: blur(20px); + background-color: var(--t-background-transparent-lighter); + border: 1px solid var(--t-border-color-strong); + border-radius: var(--t-border-radius-sm); + display: inline-flex; + gap: 2px; + padding: 2px; + + &:hover { + box-shadow: var(--t-box-shadow-light); + } + + &[data-disabled] { + background-color: inherit; + } +} diff --git a/packages/twenty-ui/src/input/button/components/IconButtonGroup.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/IconButtonGroup.module.scss.d.ts new file mode 100644 index 0000000000..8df71b4dea --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/IconButtonGroup.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly container: 'container'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/IconButtonGroup.tsx b/packages/twenty-ui/src/input/button/components/IconButtonGroup.tsx new file mode 100644 index 0000000000..9bb9c01613 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/IconButtonGroup.tsx @@ -0,0 +1,41 @@ +import { clsx } from 'clsx'; +import { type MouseEvent } from 'react'; + +import { type IconComponent } from '@ui/display'; +import { InsideButton } from '@ui/input/button/components/InsideButton'; + +import styles from './IconButtonGroup.module.scss'; + +export type IconButtonGroupProps = { + disabled?: boolean; + iconButtons: { + Icon: IconComponent; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onClick?: (event: MouseEvent) => void; + }[]; + className?: string; +}; + +export const IconButtonGroup = ({ + iconButtons, + disabled, + className, +}: IconButtonGroupProps) => { + return ( +
+ {iconButtons.map(({ Icon, onClick }, index) => { + return ( + + ); + })} +
+ ); +}; diff --git a/packages/twenty-ui/src/input/button/components/InsideButton.module.scss b/packages/twenty-ui/src/input/button/components/InsideButton.module.scss new file mode 100644 index 0000000000..af2ebbb5fc --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/InsideButton.module.scss @@ -0,0 +1,24 @@ +.button { + align-items: center; + border: none; + background-color: transparent; + border-radius: var(--t-border-radius-xs); + color: var(--t-font-color-tertiary); + cursor: pointer; + display: flex; + flex-direction: row; + height: 20px; + justify-content: center; + padding: 0; + white-space: nowrap; + min-width: 20px; + transition: background-color 0.1s ease; + + &:hover { + background-color: var(--t-background-transparent-light); + } + + &[data-disabled] { + cursor: not-allowed; + } +} diff --git a/packages/twenty-ui/src/input/button/components/InsideButton.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/InsideButton.module.scss.d.ts new file mode 100644 index 0000000000..d6e2364fc5 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/InsideButton.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly button: 'button'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/InsideButton.tsx b/packages/twenty-ui/src/input/button/components/InsideButton.tsx new file mode 100644 index 0000000000..7edb37ac79 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/InsideButton.tsx @@ -0,0 +1,34 @@ +import { clsx } from 'clsx'; +import React, { useContext } from 'react'; + +import { type IconComponent } from '@ui/display'; +import { ThemeContext } from '@ui/theme-constants'; + +import styles from './InsideButton.module.scss'; + +export type InsideButtonProps = { + className?: string; + Icon?: IconComponent; + onClick?: (event: React.MouseEvent) => void; + disabled?: boolean; +}; + +export const InsideButton = ({ + className, + Icon, + onClick, + disabled = false, +}: InsideButtonProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/input/button/components/LightButton.module.scss b/packages/twenty-ui/src/input/button/components/LightButton.module.scss new file mode 100644 index 0000000000..603cb741bb --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/LightButton.module.scss @@ -0,0 +1,74 @@ +// State rules below only assign CSS custom properties; the actual +// declarations live on the flat .button class so twenty-front +// styled(LightButton) overrides keep beating them at (0,1,0) specificity, +// exactly like the legacy single Linaria class did. +// Cascade order replicates the legacy Linaria color switch precedence: +// (active || focus) > disabled > accent base — later rules win. + +.button { + align-items: center; + background: transparent; + border: var(--light-button-border, none); + border-radius: var(--t-border-radius-sm); + box-shadow: var(--light-button-box-shadow, none); + color: var(--light-button-color); + cursor: pointer; + display: flex; + flex-direction: row; + font-family: var(--t-font-family); + font-weight: var(--t-font-weight-regular); + gap: var(--t-spacing-1); + height: 24px; + padding: 0 var(--t-spacing-2); + transition: background 0.1s ease; + white-space: nowrap; + + &:hover { + background: var( + --light-button-hover-bg, + var(--t-background-transparent-light) + ); + } + + &:focus { + outline: none; + } + + &:active { + background: var( + --light-button-active-bg, + var(--t-background-transparent-medium) + ); + } + + &[data-disabled] { + cursor: not-allowed; + } +} + +.button[data-accent='secondary'] { + --light-button-color: var(--t-font-color-secondary); +} + +.button[data-accent='tertiary'] { + --light-button-color: var(--t-font-color-tertiary); +} + +.button[data-disabled] { + --light-button-color: var(--t-font-color-extra-light); + --light-button-hover-bg: transparent; + --light-button-active-bg: transparent; +} + +// active || focus turn the text blue even when disabled, matching the legacy +// switch where the (active || focus) branch came first. +.button[data-active], +.button[data-focus] { + --light-button-color: var(--t-color-blue); +} + +// focus is gated by !disabled in the component, like the legacy call-site. +.button[data-focus] { + --light-button-border: 1px solid var(--t-color-blue); + --light-button-box-shadow: 0 0 0 3px var(--t-color-blue3); +} diff --git a/packages/twenty-ui/src/input/button/components/LightButton.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/LightButton.module.scss.d.ts new file mode 100644 index 0000000000..d6e2364fc5 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/LightButton.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly button: 'button'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/LightButton.tsx b/packages/twenty-ui/src/input/button/components/LightButton.tsx new file mode 100644 index 0000000000..118afaae8b --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/LightButton.tsx @@ -0,0 +1,51 @@ +import { clsx } from 'clsx'; +import { type MouseEvent, useContext } from 'react'; + +import { type IconComponent } from '@ui/display'; +import { ThemeContext } from '@ui/theme-constants'; + +import styles from './LightButton.module.scss'; + +export type LightButtonAccent = 'secondary' | 'tertiary'; + +export type LightButtonProps = { + className?: string; + Icon?: IconComponent; + title?: string; + accent?: LightButtonAccent; + active?: boolean; + disabled?: boolean; + focus?: boolean; + onClick?: (event: MouseEvent) => void; + type?: React.ComponentProps<'button'>['type']; +}; + +export const LightButton = ({ + className, + Icon, + title, + active = false, + accent = 'secondary', + disabled = false, + focus = false, + type = 'button', + onClick, +}: LightButtonProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/input/button/components/LightIconButton.module.scss b/packages/twenty-ui/src/input/button/components/LightIconButton.module.scss new file mode 100644 index 0000000000..b2c4b7c963 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/LightIconButton.module.scss @@ -0,0 +1,86 @@ +// State rules below only assign CSS custom properties; the actual +// declarations live on the flat .button class so twenty-front +// styled(LightIconButton) overrides keep beating them at (0,1,0) specificity, +// exactly like the legacy single Linaria class did. +// Cascade order replicates the legacy Linaria color switch precedence: +// (active || focus) > disabled > accent base — later rules win. + +.button { + align-items: center; + background: transparent; + border: var(--light-icon-button-border, none); + border-radius: var(--t-border-radius-sm); + box-shadow: var(--light-icon-button-box-shadow, none); + color: var(--light-icon-button-color); + cursor: pointer; + display: flex; + flex-direction: row; + font-family: var(--t-font-family); + font-weight: var(--t-font-weight-regular); + gap: var(--t-spacing-1); + justify-content: center; + padding: var(--t-spacing-1); + transition: background 0.1s ease; + white-space: nowrap; + + &:hover { + background: var( + --light-icon-button-hover-bg, + var(--t-background-transparent-light) + ); + } + + &:focus { + outline: none; + } + + &:active { + background: var( + --light-icon-button-active-bg, + var(--t-background-transparent-medium) + ); + } + + &[data-disabled] { + cursor: not-allowed; + } +} + +.small { + height: var(--t-spacing-6); + min-width: var(--t-spacing-6); + width: var(--t-spacing-6); +} + +.medium { + height: var(--t-spacing-8); + min-width: var(--t-spacing-8); + width: var(--t-spacing-8); +} + +.button[data-accent='secondary'] { + --light-icon-button-color: var(--t-font-color-secondary); +} + +.button[data-accent='tertiary'] { + --light-icon-button-color: var(--t-font-color-tertiary); +} + +.button[data-disabled] { + --light-icon-button-color: var(--t-font-color-extra-light); + --light-icon-button-hover-bg: transparent; + --light-icon-button-active-bg: transparent; +} + +// active || focus turn the icon blue even when disabled, matching the legacy +// switch where the (active || focus) branch came first. +.button[data-active], +.button[data-focus] { + --light-icon-button-color: var(--t-color-blue); +} + +// focus is gated by !disabled in the component, like the legacy call-site. +.button[data-focus] { + --light-icon-button-border: 1px solid var(--t-color-blue); + --light-icon-button-box-shadow: 0 0 0 3px var(--t-color-blue3); +} diff --git a/packages/twenty-ui/src/input/button/components/LightIconButton.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/LightIconButton.module.scss.d.ts new file mode 100644 index 0000000000..de85059fc8 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/LightIconButton.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly button: 'button'; + readonly small: 'small'; + readonly medium: 'medium'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/LightIconButton.tsx b/packages/twenty-ui/src/input/button/components/LightIconButton.tsx new file mode 100644 index 0000000000..7092d18a71 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/LightIconButton.tsx @@ -0,0 +1,60 @@ +import { clsx } from 'clsx'; +import { type ComponentProps, type MouseEvent, useContext } from 'react'; + +import { type IconComponent } from '@ui/display'; +import { ThemeContext } from '@ui/theme-constants'; + +import styles from './LightIconButton.module.scss'; + +export type LightIconButtonAccent = 'secondary' | 'tertiary'; +export type LightIconButtonSize = 'small' | 'medium'; + +export type LightIconButtonProps = { + className?: string; + testId?: string; + Icon?: IconComponent; + title?: string; + size?: LightIconButtonSize; + accent?: LightIconButtonAccent; + active?: boolean; + disabled?: boolean; + focus?: boolean; + onClick?: (event: MouseEvent) => void; +} & Pick, 'aria-label' | 'title'>; + +export const LightIconButton = ({ + 'aria-label': ariaLabel, + className, + testId, + Icon, + active = false, + size = 'small', + accent = 'secondary', + disabled = false, + focus = false, + onClick, + title, +}: LightIconButtonProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/input/button/components/LightIconButtonGroup.module.scss b/packages/twenty-ui/src/input/button/components/LightIconButtonGroup.module.scss new file mode 100644 index 0000000000..c4961b98a9 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/LightIconButtonGroup.module.scss @@ -0,0 +1,4 @@ +.container { + display: inline-flex; + gap: 2px; +} diff --git a/packages/twenty-ui/src/input/button/components/LightIconButtonGroup.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/LightIconButtonGroup.module.scss.d.ts new file mode 100644 index 0000000000..8df71b4dea --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/LightIconButtonGroup.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly container: 'container'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/LightIconButtonGroup.tsx b/packages/twenty-ui/src/input/button/components/LightIconButtonGroup.tsx new file mode 100644 index 0000000000..b4a0847f88 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/LightIconButtonGroup.tsx @@ -0,0 +1,62 @@ +import { + type FunctionComponent, + type MouseEvent, + type ReactElement, +} from 'react'; + +import { type IconComponent } from '@ui/display'; + +import { clsx } from 'clsx'; +import { LightIconButton, type LightIconButtonProps } from './LightIconButton'; + +import styles from './LightIconButtonGroup.module.scss'; + +export type LightIconButtonGroupProps = Pick< + LightIconButtonProps, + 'className' | 'size' +> & { + iconButtons: { + Wrapper?: FunctionComponent<{ iconButton: ReactElement }>; + Icon: IconComponent; + accent?: LightIconButtonProps['accent']; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onClick?: (event: MouseEvent) => void; + disabled?: boolean; + ariaLabel?: string; + dataTestId?: string; + }[]; +}; + +export const LightIconButtonGroup = ({ + iconButtons, + size, + className, +}: LightIconButtonGroupProps) => ( +
+ {iconButtons.map( + ({ Wrapper, Icon, accent, onClick, ariaLabel, dataTestId }, index) => { + const iconButton = ( + + ); + + return Wrapper ? ( + + ) : ( + iconButton + ); + }, + )} +
+); diff --git a/packages/twenty-ui/src/input/button/components/MainButton.module.scss b/packages/twenty-ui/src/input/button/components/MainButton.module.scss new file mode 100644 index 0000000000..d81831a339 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/MainButton.module.scss @@ -0,0 +1,62 @@ +// State rules below only assign CSS custom properties; the actual +// declarations live on the flat .button class so twenty-front +// styled(MainButton) overrides keep beating them at (0,1,0) specificity, +// exactly like the legacy single Linaria class did. +// Cascade order replicates the legacy switches: disabled > variant. + +.button { + align-items: center; + background: var(--main-button-bg); + border: 1px solid; + border-color: var(--main-button-border-color); + border-radius: var(--t-border-radius-md); + box-shadow: var(--main-button-box-shadow, var(--t-box-shadow-light)); + color: var(--main-button-color); + cursor: pointer; + display: flex; + flex-direction: row; + font-family: var(--t-font-family); + font-weight: var(--t-font-weight-semi-bold); + gap: var(--t-spacing-2); + justify-content: center; + outline: none; + padding: var(--t-spacing-2) var(--t-spacing-3); + max-height: var(--t-spacing-8); + width: var(--main-button-width, auto); + + &:hover { + background: var(--main-button-hover-bg); + } + + &[data-disabled] { + cursor: not-allowed; + } +} + +.button[data-variant='primary'] { + --main-button-bg: var(--t-background-primary-inverted); + --main-button-border-color: var(--t-background-transparent-strong); + --main-button-color: var(--t-font-color-inverted); + --main-button-hover-bg: var(--t-background-primary-inverted-hover); +} + +.button[data-variant='secondary'] { + --main-button-bg: var(--t-background-primary); + --main-button-border-color: var(--t-border-color-medium); + --main-button-color: var(--t-font-color-primary); + --main-button-hover-bg: var(--t-background-tertiary); +} + +.button[data-disabled] { + --main-button-bg: var(--t-background-secondary); + --main-button-border-color: var(--t-background-transparent-lighter); + --main-button-box-shadow: none; + --main-button-color: var(--t-font-color-light); +} + +// hover when disabled: the legacy hover switch ignores disabled for +// secondary (keeps tertiary), while primary falls back to the disabled +// background. +.button[data-variant='primary'][data-disabled] { + --main-button-hover-bg: var(--t-background-secondary); +} diff --git a/packages/twenty-ui/src/input/button/components/MainButton.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/MainButton.module.scss.d.ts new file mode 100644 index 0000000000..d6e2364fc5 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/MainButton.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly button: 'button'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/MainButton.tsx b/packages/twenty-ui/src/input/button/components/MainButton.tsx new file mode 100644 index 0000000000..24ca26c40e --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/MainButton.tsx @@ -0,0 +1,58 @@ +import { clsx } from 'clsx'; +import React, { type FunctionComponent, useContext } from 'react'; + +import { type IconComponent } from '@ui/display'; +import { ThemeContext } from '@ui/theme-constants'; + +import styles from './MainButton.module.scss'; + +export type MainButtonVariant = 'primary' | 'secondary'; + +type Props = { + title: string; + fullWidth?: boolean; + width?: number; + variant?: MainButtonVariant; + soon?: boolean; +} & React.ComponentProps<'button'>; + +type MainButtonProps = Props & { + Icon?: IconComponent | FunctionComponent<{ size: number }>; +}; + +export const MainButton = ({ + Icon, + title, + width, + fullWidth = false, + variant = 'primary', + type, + onClick, + disabled, + className, +}: MainButtonProps) => { + const { theme } = useContext(ThemeContext); + + // Replicates the legacy ternary exactly: fullWidth wins over width, and a + // falsy width (0) falls back to the 'auto' default. + const widthValue = fullWidth ? '100%' : width ? `${width}px` : undefined; + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/input/button/components/RoundedIconButton.module.scss b/packages/twenty-ui/src/input/button/components/RoundedIconButton.module.scss new file mode 100644 index 0000000000..d238375e8a --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/RoundedIconButton.module.scss @@ -0,0 +1,35 @@ +.button { + align-items: center; + background: var(--t-color-blue); + border: none; + border-radius: 50%; + color: var(--t-font-color-inverted); + cursor: pointer; + display: flex; + justify-content: center; + outline: none; + padding: 0; + transition: + color 0.1s ease-in-out, + background 0.1s ease-in-out; + + &:hover:not(:disabled) { + background: var(--t-color-blue10); + } + + &:disabled { + background: var(--t-background-quaternary); + color: var(--t-font-color-tertiary); + cursor: default; + } +} + +.small { + height: 20px; + width: 20px; +} + +.medium { + height: 24px; + width: 24px; +} diff --git a/packages/twenty-ui/src/input/button/components/RoundedIconButton.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/RoundedIconButton.module.scss.d.ts new file mode 100644 index 0000000000..de85059fc8 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/RoundedIconButton.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly button: 'button'; + readonly small: 'small'; + readonly medium: 'medium'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/RoundedIconButton.tsx b/packages/twenty-ui/src/input/button/components/RoundedIconButton.tsx new file mode 100644 index 0000000000..a37a58e1fd --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/RoundedIconButton.tsx @@ -0,0 +1,34 @@ +import { clsx } from 'clsx'; +import React, { useContext } from 'react'; + +import { type IconComponent } from '@ui/display'; +import { ThemeContext } from '@ui/theme-constants'; + +import styles from './RoundedIconButton.module.scss'; + +export type RoundedIconButtonSize = 'small' | 'medium'; + +type RoundedIconButtonProps = { + Icon: IconComponent; + size?: RoundedIconButtonSize; +} & React.ButtonHTMLAttributes; + +export const RoundedIconButton = ({ + Icon, + onClick, + disabled, + className, + size = 'small', +}: RoundedIconButtonProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/input/button/components/TabButton/TabButton.module.scss b/packages/twenty-ui/src/input/button/components/TabButton/TabButton.module.scss new file mode 100644 index 0000000000..3b898f5cf1 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/TabButton/TabButton.module.scss @@ -0,0 +1,3 @@ +.tabTooltipWrapper { + display: flex; +} diff --git a/packages/twenty-ui/src/input/button/components/TabButton/TabButton.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/TabButton/TabButton.module.scss.d.ts new file mode 100644 index 0000000000..9bc27db801 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/TabButton/TabButton.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly tabTooltipWrapper: 'tabTooltipWrapper'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/TabButton/TabButton.tsx b/packages/twenty-ui/src/input/button/components/TabButton/TabButton.tsx new file mode 100644 index 0000000000..94c305f755 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/TabButton/TabButton.tsx @@ -0,0 +1,77 @@ +import { AppTooltip, type IconComponent, TooltipDelay } from '@ui/display'; +import { StyledTabButton } from '@ui/input/button/components/TabButton/internals/components/StyledTabBase'; +import { TabContent } from '@ui/input/button/components/TabButton/internals/components/TabContent'; +import { type ReactElement } from 'react'; + +import styles from './TabButton.module.scss'; + +type TabButtonProps = { + id: string; + active?: boolean; + disabled?: boolean; + to?: string; + LeftIcon?: IconComponent; + className?: string; + title?: string; + onClick?: () => void; + logo?: string; + RightIcon?: IconComponent; + pill?: string | ReactElement; + contentSize?: 'sm' | 'md'; + disableTestId?: boolean; + tooltipContent?: string; +}; + +export const TabButton = ({ + id, + active, + disabled, + to, + LeftIcon, + className, + title, + onClick, + logo, + RightIcon, + pill, + contentSize = 'sm', + disableTestId = false, + tooltipContent, +}: TabButtonProps) => { + const tabElementId = `tab-${id}`; + + return ( +
+ + + + {tooltipContent && ( + + )} +
+ ); +}; diff --git a/packages/twenty-ui/src/input/button/components/TabButton/internals/components/StyledTabBase.module.scss b/packages/twenty-ui/src/input/button/components/TabButton/internals/components/StyledTabBase.module.scss new file mode 100644 index 0000000000..26e78ff720 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/TabButton/internals/components/StyledTabBase.module.scss @@ -0,0 +1,100 @@ +.tabButton { + all: unset; + align-items: center; + color: var(--t-font-color-secondary); + cursor: pointer; + background-color: transparent; + border: none; + font-family: inherit; + display: flex; + gap: var(--t-spacing-1); + justify-content: center; + text-decoration: none; + position: relative; + + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background-color: transparent; + z-index: 1; + } + + &[data-disabled] { + color: var(--t-font-color-light); + pointer-events: none; + } + + // active wins over disabled, matching the legacy Linaria ternary + // (active ? primary : disabled ? light : secondary) + &[data-active] { + color: var(--t-font-color-primary); + + &::after { + background-color: var(--t-border-color-inverted); + } + } +} + +.tabContainer { + align-items: center; + color: var(--t-font-color-secondary); + cursor: pointer; + background-color: transparent; + display: flex; + gap: var(--t-spacing-1); + justify-content: center; + text-decoration: none; + position: relative; + + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background-color: transparent; + z-index: 1; + } + + &[data-disabled] { + color: var(--t-font-color-light); + } + + &[data-active] { + color: var(--t-font-color-primary); + + &::after { + background-color: var(--t-border-color-inverted); + } + } +} + +.tabHover { + box-sizing: border-box; + display: flex; + gap: var(--t-spacing-1); + // the legacy ternary fell through to the md padding when contentSize was + // undefined, so md padding is the base and sm is the override + padding: var(--t-spacing-2) var(--t-spacing-2); + font-weight: var(--t-font-weight-medium); + width: 100%; + white-space: nowrap; + border-radius: var(--t-border-radius-sm); + + &[data-content-size='sm'] { + padding: var(--t-spacing-1) var(--t-spacing-2); + } + + &:hover { + background: var(--t-background-tertiary); + } + + &:active { + background: var(--t-background-quaternary); + } +} diff --git a/packages/twenty-ui/src/input/button/components/TabButton/internals/components/StyledTabBase.module.scss.d.ts b/packages/twenty-ui/src/input/button/components/TabButton/internals/components/StyledTabBase.module.scss.d.ts new file mode 100644 index 0000000000..9832b4d27d --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/TabButton/internals/components/StyledTabBase.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly tabButton: 'tabButton'; + readonly tabContainer: 'tabContainer'; + readonly tabHover: 'tabHover'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/button/components/TabButton/internals/components/StyledTabBase.tsx b/packages/twenty-ui/src/input/button/components/TabButton/internals/components/StyledTabBase.tsx new file mode 100644 index 0000000000..ffe8f44ef8 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/TabButton/internals/components/StyledTabBase.tsx @@ -0,0 +1,84 @@ +import { clsx } from 'clsx'; +import { type ComponentPropsWithoutRef, forwardRef } from 'react'; +import { Link } from 'react-router-dom'; + +import styles from './StyledTabBase.module.scss'; + +// The deprecated Linaria styled components forwarded refs and arbitrary +// native props (twenty-front spreads drag-and-drop props onto +// StyledTabContainer), so the ports preserve that contract. +type StyledTabButtonProps = { + active?: boolean; + disabled?: boolean; + to?: string; +} & ComponentPropsWithoutRef<'button'>; + +export const StyledTabButton = forwardRef( + ({ active, disabled, to, className, children, ...rest }, ref) => { + // Replaces the legacy Linaria `as` polymorphism: react-router Link when a + // `to` is provided, a native button otherwise. Typed as any to forward all + // props untyped, exactly like the legacy `as` prop did. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const TabButtonComponent: any = to ? Link : 'button'; + + return ( + + {children} + + ); + }, +); + +StyledTabButton.displayName = 'StyledTabButton'; + +type StyledTabContainerProps = { + active?: boolean; + disabled?: boolean; +} & ComponentPropsWithoutRef<'div'>; + +export const StyledTabContainer = forwardRef< + HTMLDivElement, + StyledTabContainerProps +>(({ active, disabled, className, children, ...rest }, ref) => ( +
+ {children} +
+)); + +StyledTabContainer.displayName = 'StyledTabContainer'; + +type StyledTabHoverProps = { + contentSize?: 'sm' | 'md'; +} & ComponentPropsWithoutRef<'span'>; + +export const StyledTabHover = forwardRef( + ({ contentSize, className, children, ...rest }, ref) => ( + + {children} + + ), +); + +StyledTabHover.displayName = 'StyledTabHover'; diff --git a/packages/twenty-ui/src/input/button/components/TabButton/internals/components/TabContent.tsx b/packages/twenty-ui/src/input/button/components/TabButton/internals/components/TabContent.tsx new file mode 100644 index 0000000000..9ca002675e --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/TabButton/internals/components/TabContent.tsx @@ -0,0 +1,48 @@ +import { Pill } from '@ui/components/Pill/Pill'; +import { Avatar, type IconComponent } from '@ui/display'; +import { ThemeContext } from '@ui/theme-constants'; +import { type ReactElement, useContext } from 'react'; +import { StyledTabHover } from './StyledTabBase'; + +export type TabContentProps = { + id: string; + active?: boolean; + disabled?: boolean; + LeftIcon?: IconComponent; + title?: string; + logo?: string; + RightIcon?: IconComponent; + pill?: string | ReactElement; + contentSize?: 'sm' | 'md'; + className?: string; +}; + +export const TabContent = ({ + active, + disabled, + LeftIcon, + title, + logo, + RightIcon, + pill, + contentSize = 'sm', + className, +}: TabContentProps) => { + const { theme } = useContext(ThemeContext); + + const iconColor = active + ? theme.font.color.primary + : disabled + ? theme.font.color.extraLight + : theme.font.color.secondary; + + return ( + + {LeftIcon && } + {logo && } + {title} + {RightIcon && } + {pill && (typeof pill === 'string' ? : pill)} + + ); +}; diff --git a/packages/twenty-ui/src/input/button/components/__stories__/AnimatedLightIconButton.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/AnimatedLightIconButton.stories.tsx new file mode 100644 index 0000000000..c9f4a8e2b2 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/__stories__/AnimatedLightIconButton.stories.tsx @@ -0,0 +1,110 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { IconSearch } from '@ui/display'; +import { + type LightIconButtonAccent, + type LightIconButtonSize, +} from '@ui/input/button/components/LightIconButton'; +import { + CatalogDecorator, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; +import { AnimatedLightIconButton } from '../AnimatedLightIconButton'; + +const meta: Meta = { + title: 'UI/Input/Button/AnimatedLightIconButton', + component: AnimatedLightIconButton, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: 'Filter', + accent: 'secondary', + disabled: false, + active: false, + focus: false, + Icon: IconSearch, + animate: { scale: 1.2 }, + transition: { duration: 0.3 }, + }, + argTypes: { + Icon: { control: false }, + animate: { control: 'object' }, + transition: { control: 'object' }, + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: { + title: 'Filter', + Icon: IconSearch, + animate: { scale: 1.2 }, + transition: { duration: 0.3 }, + }, + argTypes: { + accent: { control: false }, + disabled: { control: false }, + active: { control: false }, + focus: { control: false }, + animate: { control: 'object' }, + transition: { control: 'object' }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.pressed'] }, + catalog: { + dimensions: [ + { + name: 'states', + values: [ + 'default', + 'hover', + 'pressed', + 'disabled', + 'active', + 'focus', + 'disabled+focus', + 'disabled+active', + ], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + case 'pressed': + return { className: state }; + case 'focus': + return { focus: true }; + case 'disabled': + return { disabled: true }; + case 'active': + return { active: true }; + case 'disabled+focus': + return { disabled: true, focus: true }; + case 'disabled+active': + return { disabled: true, active: true }; + default: + return {}; + } + }, + }, + { + name: 'accents', + values: ['secondary', 'tertiary'] satisfies LightIconButtonAccent[], + props: (accent: LightIconButtonAccent) => ({ accent }), + }, + { + name: 'sizes', + values: ['small', 'medium'] satisfies LightIconButtonSize[], + props: (size: LightIconButtonSize) => ({ size }), + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/input/button/components/__stories__/Button.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/Button.stories.tsx new file mode 100644 index 0000000000..9de181ff91 --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/__stories__/Button.stories.tsx @@ -0,0 +1,368 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { IconReload, IconSearch } from '@ui/display'; +import { + CatalogDecorator, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; +import { + Button, + type ButtonAccent, + type ButtonPosition, + type ButtonSize, + type ButtonVariant, +} from '../Button/Button'; + +const meta: Meta = { + title: 'UI/Input/Button/Button', + component: Button, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + argTypes: { + hotkeys: { control: false }, + Icon: { control: false }, + }, + args: { + title: 'Button', + size: 'small', + variant: 'primary', + inverted: false, + accent: 'danger', + disabled: false, + focus: false, + fullWidth: false, + soon: false, + position: 'standalone', + Icon: IconSearch, + className: '', + isLoading: false, + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: { title: 'Filter', Icon: IconSearch, hotkeys: ['⌘', 'O'] }, + argTypes: { + size: { control: false }, + variant: { control: false }, + accent: { control: false }, + disabled: { control: false }, + focus: { control: false }, + fullWidth: { control: false }, + soon: { control: false }, + position: { control: false }, + className: { control: false }, + isLoading: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, + catalog: { + dimensions: [ + { + name: 'sizes', + values: ['small', 'medium'] satisfies ButtonSize[], + props: (size: ButtonSize) => ({ size }), + }, + { + name: 'states', + values: [ + 'default', + 'hover', + 'pressed', + 'disabled', + 'focus', + 'disabled+focus', + ], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + case 'pressed': + return { className: state }; + case 'focus': + return { focus: true }; + case 'disabled': + return { disabled: true }; + case 'active': + return { active: true }; + case 'disabled+focus': + return { focus: true, disabled: true }; + default: + return {}; + } + }, + }, + { + name: 'accents', + values: ['default', 'blue', 'danger'] satisfies ButtonAccent[], + props: (accent: ButtonAccent) => ({ accent }), + }, + { + name: 'variants', + values: [ + 'primary', + 'secondary', + 'tertiary', + ] satisfies ButtonVariant[], + props: (variant: ButtonVariant) => ({ variant }), + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; + +export const SoonCatalog: CatalogStory = { + args: { title: 'Filter', Icon: IconSearch, soon: true }, + argTypes: { + size: { control: false }, + variant: { control: false }, + accent: { control: false }, + disabled: { control: false }, + focus: { control: false }, + fullWidth: { control: false }, + soon: { control: false }, + position: { control: false }, + className: { control: false }, + hotkeys: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, + catalog: { + dimensions: [ + { + name: 'sizes', + values: ['small', 'medium'] satisfies ButtonSize[], + props: (size: ButtonSize) => ({ size }), + }, + { + name: 'states', + values: [ + 'default', + 'hover', + 'pressed', + 'disabled', + 'focus', + 'disabled+focus', + ], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + case 'pressed': + return { className: state }; + case 'focus': + return { focus: true }; + case 'disabled': + return { disabled: true }; + case 'active': + return { active: true }; + case 'disabled+focus': + return { focus: true, disabled: true }; + default: + return {}; + } + }, + }, + { + name: 'accents', + values: ['default', 'blue', 'danger'] satisfies ButtonAccent[], + props: (accent: ButtonAccent) => ({ accent }), + }, + { + name: 'variants', + values: [ + 'primary', + 'secondary', + 'tertiary', + ] satisfies ButtonVariant[], + props: (variant: ButtonVariant) => ({ variant }), + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; + +export const PositionCatalog: CatalogStory = { + args: { title: 'Filter', Icon: IconSearch }, + argTypes: { + size: { control: false }, + variant: { control: false }, + accent: { control: false }, + disabled: { control: false }, + focus: { control: false }, + fullWidth: { control: false }, + soon: { control: false }, + position: { control: false }, + hotkeys: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, + catalog: { + dimensions: [ + { + name: 'positions', + values: [ + 'standalone', + 'left', + 'middle', + 'right', + ] satisfies ButtonPosition[], + props: (position: ButtonPosition) => ({ position }), + }, + { + name: 'states', + values: [ + 'default', + 'hover', + 'pressed', + 'disabled', + 'focus', + 'disabled+focus', + ], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + case 'pressed': + return { className: state }; + case 'focus': + return { focus: true }; + case 'disabled': + return { disabled: true }; + case 'active': + return { active: true }; + case 'disabled+focus': + return { focus: true, disabled: true }; + default: + return {}; + } + }, + }, + { + name: 'sizes', + values: ['small', 'medium'] satisfies ButtonSize[], + props: (size: ButtonSize) => ({ size }), + }, + { + name: 'variants', + values: [ + 'primary', + 'secondary', + 'tertiary', + ] satisfies ButtonVariant[], + props: (variant: ButtonVariant) => ({ variant }), + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; + +export const ShortcutCatalog: CatalogStory = { + args: { title: 'Actions', hotkeys: ['⌘', 'O'] }, + argTypes: { + size: { control: false }, + variant: { control: false }, + accent: { control: false }, + disabled: { control: false }, + focus: { control: false }, + fullWidth: { control: false }, + soon: { control: false }, + position: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, + catalog: { + dimensions: [ + { + name: 'sizes', + values: ['small', 'medium'] satisfies ButtonSize[], + props: (size: ButtonSize) => ({ size }), + }, + { + name: 'accents', + values: ['default', 'blue', 'danger'] satisfies ButtonAccent[], + props: (accent: ButtonAccent) => ({ accent }), + }, + { + name: 'variants', + values: [ + 'primary', + 'secondary', + 'tertiary', + ] satisfies ButtonVariant[], + props: (variant: ButtonVariant) => ({ variant }), + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; + +export const FullWidth: Story = { + args: { title: 'Filter', Icon: IconSearch, fullWidth: true }, + argTypes: { + size: { control: false }, + variant: { control: false }, + accent: { control: false }, + focus: { control: false }, + disabled: { control: false }, + fullWidth: { control: false }, + soon: { control: false }, + position: { control: false }, + className: { control: false }, + Icon: { control: false }, + }, + decorators: [ComponentDecorator], +}; + +export const LoadingButton: Story = { + args: { + title: 'Reload', + Icon: IconReload, + isLoading: true, + }, + argTypes: { + size: { control: false }, + variant: { control: false }, + accent: { control: false }, + focus: { control: false }, + disabled: { control: false }, + fullWidth: { control: false }, + soon: { control: false }, + position: { control: false }, + className: { control: false }, + isLoading: { control: 'boolean' }, + }, + parameters: { + catalog: { + isLoading: [ + { + name: 'isLoading', + values: [true, false] satisfies boolean[], + props: (value: boolean) => ({ isLoading: value }), + }, + ], + }, + }, + decorators: [ComponentDecorator], +}; diff --git a/packages/twenty-ui/src/input/button/components/__stories__/ButtonGroup.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/ButtonGroup.stories.tsx new file mode 100644 index 0000000000..ef2ddea9da --- /dev/null +++ b/packages/twenty-ui/src/input/button/components/__stories__/ButtonGroup.stories.tsx @@ -0,0 +1,86 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { IconCheckbox, IconNotes, IconTimelineEvent } from '@ui/display'; +import { + CatalogDecorator, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; +import { + Button, + type ButtonAccent, + type ButtonSize, + type ButtonVariant, +} from '../Button/Button'; +import { ButtonGroup } from '../ButtonGroup'; + +const meta: Meta = { + title: 'UI/Input/Button/ButtonGroup', + component: ButtonGroup, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + size: 'small', + variant: 'primary', + accent: 'danger', + children: [ + + ); +}; diff --git a/packages/twenty-ui/src/input/components/Checkbox.module.scss b/packages/twenty-ui/src/input/components/Checkbox.module.scss new file mode 100644 index 0000000000..e2d79849ba --- /dev/null +++ b/packages/twenty-ui/src/input/components/Checkbox.module.scss @@ -0,0 +1,141 @@ +.root { + --checkbox-box-size: 12px; + --checkbox-icon-size: 14px; + --checkbox-border-width: 1px; + --checkbox-bg: transparent; + --checkbox-border-color: var(--t-border-color-secondary-inverted); + --checkbox-box-radius: var(--t-border-radius-sm); + + align-items: center; + border-radius: var(--t-border-radius-md); + cursor: pointer; + display: flex; + padding: 0; + position: relative; +} + +.primary { + --checkbox-border-color: var(--t-border-color-inverted); +} + +.secondary { + --checkbox-border-color: var(--t-border-color-secondary-inverted); +} + +.tertiary { + --checkbox-border-color: var(--t-border-color-medium); + --checkbox-border-width: 1.43px; +} + +.small { + --checkbox-box-size: 12px; + --checkbox-icon-size: 14px; +} + +.large { + --checkbox-box-size: 18px; + --checkbox-icon-size: 20px; + --checkbox-border-width: 1.43px; +} + +.squared { + --checkbox-box-radius: var(--t-border-radius-sm); + + border-radius: var(--t-border-radius-md); +} + +.rounded { + --checkbox-box-radius: var(--t-border-radius-rounded); + + border-radius: var(--t-border-radius-rounded); +} + +.blue { + --checkbox-accent: var(--t-color-blue); + --checkbox-accent-disabled: var(--t-color-blue7); + --checkbox-accent-hover: var(--t-background-transparent-blue); +} + +.orange { + --checkbox-accent: var(--t-color-orange); + --checkbox-accent-disabled: var(--t-color-orange7); + --checkbox-accent-hover: var(--t-background-transparent-orange); +} + +.hoverable { + padding: 5px; +} + +.hoverable.large { + padding: 6px; +} + +.hoverable:hover { + background-color: var(--t-background-transparent-light); +} + +.hoverable[data-checked]:hover, +.hoverable[data-indeterminate]:hover { + background-color: var(--checkbox-accent-hover); +} + +// Disabled border falls back to the strong border color unless checked (see below). +.root[data-disabled] { + --checkbox-border-color: var(--t-border-color-strong); + + cursor: not-allowed; +} + +// Checked/indeterminate accent wins over the variant and disabled border colors. +.root[data-checked], +.root[data-indeterminate] { + --checkbox-bg: var(--checkbox-accent); + --checkbox-border-color: var(--checkbox-accent); +} + +.root[data-disabled][data-checked], +.root[data-disabled][data-indeterminate] { + --checkbox-bg: var(--checkbox-accent-disabled); + --checkbox-border-color: var(--checkbox-accent-disabled); +} + +.hoverable[data-disabled]:hover { + background-color: transparent; +} + +.box { + height: calc(var(--checkbox-box-size) + 2px); + padding: 0; + position: relative; + width: calc(var(--checkbox-box-size) + 2px); +} + +.box::before { + background: var(--checkbox-bg); + border-color: var(--checkbox-border-color); + border-radius: var(--checkbox-box-radius); + border-style: solid; + border-width: var(--checkbox-border-width); + // The border sits outside the declared box size, so size against the content box. + box-sizing: content-box; + content: ''; + display: inline-block; + height: var(--checkbox-box-size); + width: var(--checkbox-box-size); +} + +.indicator { + display: block; + height: var(--checkbox-icon-size); + left: 0; + position: absolute; + top: 0; + width: var(--checkbox-icon-size); +} + +.indicator svg { + display: block; + height: 100%; + stroke: var(--t-font-color-inverted); + width: 100%; +} diff --git a/packages/twenty-ui/src/input/components/Checkbox.module.scss.d.ts b/packages/twenty-ui/src/input/components/Checkbox.module.scss.d.ts new file mode 100644 index 0000000000..1cd971bc6b --- /dev/null +++ b/packages/twenty-ui/src/input/components/Checkbox.module.scss.d.ts @@ -0,0 +1,16 @@ +declare const classNames: { + readonly root: 'root'; + readonly primary: 'primary'; + readonly secondary: 'secondary'; + readonly tertiary: 'tertiary'; + readonly small: 'small'; + readonly large: 'large'; + readonly squared: 'squared'; + readonly rounded: 'rounded'; + readonly blue: 'blue'; + readonly orange: 'orange'; + readonly hoverable: 'hoverable'; + readonly box: 'box'; + readonly indicator: 'indicator'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/components/Checkbox.tsx b/packages/twenty-ui/src/input/components/Checkbox.tsx new file mode 100644 index 0000000000..b93f38e6b9 --- /dev/null +++ b/packages/twenty-ui/src/input/components/Checkbox.tsx @@ -0,0 +1,95 @@ +import { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox'; +import { clsx } from 'clsx'; +import * as React from 'react'; + +import { IconCheck, IconMinus } from '@ui/display/icon/components/TablerIcons'; + +import styles from './Checkbox.module.scss'; + +export enum CheckboxVariant { + Primary = 'primary', + Secondary = 'secondary', + Tertiary = 'tertiary', +} + +export enum CheckboxShape { + Squared = 'squared', + Rounded = 'rounded', +} + +export enum CheckboxSize { + Large = 'large', + Small = 'small', +} + +export enum CheckboxAccent { + Blue = 'blue', + Orange = 'orange', +} + +type CheckboxProps = { + checked: boolean; + indeterminate?: boolean; + hoverable?: boolean; + onChange?: (event: React.ChangeEvent) => void; + onCheckedChange?: (value: boolean) => void; + variant?: CheckboxVariant; + size?: CheckboxSize; + shape?: CheckboxShape; + className?: string; + disabled?: boolean; + accent?: CheckboxAccent; +}; + +export const Checkbox = ({ + checked, + onChange, + onCheckedChange, + indeterminate, + variant = CheckboxVariant.Primary, + size = CheckboxSize.Small, + shape = CheckboxShape.Squared, + hoverable = true, + className, + disabled = false, + accent = CheckboxAccent.Blue, +}: CheckboxProps) => { + const [isInternalChecked, setIsInternalChecked] = + React.useState(false); + + React.useEffect(() => { + setIsInternalChecked(checked ?? false); + }, [checked]); + + return ( + { + onChange?.( + eventDetails.event as unknown as React.ChangeEvent, + ); + onCheckedChange?.(value); + setIsInternalChecked(value); + }} + className={clsx( + styles.root, + styles[variant], + styles[size], + styles[shape], + styles[accent], + hoverable && styles.hoverable, + className, + )} + > + + + {indeterminate ? : } + + + + ); +}; diff --git a/packages/twenty-ui/src/input/components/IconListViewGrip.tsx b/packages/twenty-ui/src/input/components/IconListViewGrip.tsx new file mode 100644 index 0000000000..c7f4b344bf --- /dev/null +++ b/packages/twenty-ui/src/input/components/IconListViewGrip.tsx @@ -0,0 +1,11 @@ +import IconListViewGripRaw from '@assets/misc/list-view-grip.svg?react'; +import { type IconComponentProps } from '@ui/display/icon/types/IconComponent'; + +type IconListViewGripProps = Pick; + +export const IconListViewGrip = (props: IconListViewGripProps) => { + const width = props.size ?? 8; + const height = props.size ?? 32; + + return ; +}; diff --git a/packages/twenty-ui/src/input/components/Radio.module.scss b/packages/twenty-ui/src/input/components/Radio.module.scss new file mode 100644 index 0000000000..e137561262 --- /dev/null +++ b/packages/twenty-ui/src/input/components/Radio.module.scss @@ -0,0 +1,89 @@ +.container { + align-items: center; + display: inline-flex; + flex-direction: row; +} + +.containerLabelLeft { + flex-direction: row-reverse; +} + +.radio { + background-color: transparent; + border: 1px solid var(--t-font-color-secondary); + border-radius: var(--t-border-radius-rounded); + // The border sits outside the declared size, matching the legacy input rendering. + box-sizing: content-box; + display: block; + margin: 0 0 0 3px; + position: relative; + // Replaces the legacy framer-motion spring scale micro-animation. + transform: scale(0.95); + transition: transform duration(normal) ease; +} + +.small { + height: 16px; + width: 16px; +} + +.large { + height: 18px; + width: 18px; +} + +.radio:hover { + background-color: var(--t-background-tertiary); + outline: 4px solid var(--t-background-tertiary); +} + +.radio[data-checked] { + background-color: var(--t-color-blue); + border: none; + transform: scale(1.05); +} + +.radio[data-checked]:hover { + outline-color: var(--t-color-transparent-blue2); +} + +.radio[data-checked]::after { + background-color: var(--t-gray-scale-gray1); + border-radius: 50%; + content: ''; + height: 6px; + left: 50%; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: 6px; +} + +.large[data-checked]::after { + height: 8px; + width: 8px; +} + +.radio[data-disabled] { + cursor: not-allowed; + opacity: 0.12; +} + +.label { + color: var(--t-font-color-primary); + cursor: pointer; + font-size: var(--t-font-size-sm); + font-weight: var(--t-font-weight-regular); +} + +.labelRight { + margin-left: var(--t-spacing-2); +} + +.labelLeft { + margin-right: var(--t-spacing-2); +} + +.label[data-disabled] { + opacity: 0.32; +} diff --git a/packages/twenty-ui/src/input/components/Radio.module.scss.d.ts b/packages/twenty-ui/src/input/components/Radio.module.scss.d.ts new file mode 100644 index 0000000000..52e585117d --- /dev/null +++ b/packages/twenty-ui/src/input/components/Radio.module.scss.d.ts @@ -0,0 +1,11 @@ +declare const classNames: { + readonly container: 'container'; + readonly containerLabelLeft: 'containerLabelLeft'; + readonly radio: 'radio'; + readonly small: 'small'; + readonly large: 'large'; + readonly label: 'label'; + readonly labelRight: 'labelRight'; + readonly labelLeft: 'labelLeft'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/components/Radio.tsx b/packages/twenty-ui/src/input/components/Radio.tsx new file mode 100644 index 0000000000..79be597cc1 --- /dev/null +++ b/packages/twenty-ui/src/input/components/Radio.tsx @@ -0,0 +1,93 @@ +import { Radio as RadioPrimitive } from '@base-ui/react/radio'; +import { RadioGroup as RadioGroupPrimitive } from '@base-ui/react/radio-group'; +import { clsx } from 'clsx'; +import * as React from 'react'; + +import styles from './Radio.module.scss'; +import { RadioGroup } from './RadioGroup'; + +export enum RadioSize { + Large = 'large', + Small = 'small', +} + +export enum LabelPosition { + Left = 'left', + Right = 'right', +} + +export type RadioProps = { + checked?: boolean; + className?: string; + name?: string; + disabled?: boolean; + label?: string; + labelPosition?: LabelPosition; + onChange?: (event: React.ChangeEvent) => void; + onCheckedChange?: (checked: boolean) => void; + size?: RadioSize; + style?: React.CSSProperties; + value?: string; +}; + +export const Radio = ({ + checked, + className, + name = 'input-radio', + disabled = false, + label, + labelPosition = LabelPosition.Right, + onChange, + onCheckedChange, + size = RadioSize.Small, + value, +}: RadioProps) => { + const optionId = React.useId(); + + // 'on' mimics the native input value default when neither value nor label is set. + const radioValue = value || label || 'on'; + + return ( + { + const inputElement = eventDetails.event + .target as HTMLInputElement | null; + onChange?.( + eventDetails.event as unknown as React.ChangeEvent, + ); + onCheckedChange?.(inputElement?.checked ?? true); + }} + > + + {label && ( + + )} + + ); +}; + +Radio.Group = RadioGroup; diff --git a/packages/twenty-ui/src/input/components/RadioGroup.tsx b/packages/twenty-ui/src/input/components/RadioGroup.tsx new file mode 100644 index 0000000000..2c9a3ede74 --- /dev/null +++ b/packages/twenty-ui/src/input/components/RadioGroup.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import { themeCssVariables } from '@ui/theme-constants'; +import { type RadioProps } from './Radio'; + +type RadioGroupProps = React.PropsWithChildren & { + value?: string; + onChange?: (event: React.ChangeEvent) => void; + onValueChange?: (value: string) => void; +}; + +export const RadioGroup = ({ + value, + onChange, + onValueChange, + children, +}: RadioGroupProps) => { + const handleChange = (event: React.ChangeEvent) => { + onChange?.(event); + onValueChange?.(event.target.value); + }; + + return ( + <> + {React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child, { + style: { marginBottom: themeCssVariables.spacing[2] }, + checked: child.props.value === value, + onChange: handleChange, + }); + } + return child; + })} + + ); +}; diff --git a/packages/twenty-ui/src/input/components/SearchInput.module.scss b/packages/twenty-ui/src/input/components/SearchInput.module.scss new file mode 100644 index 0000000000..cf35289313 --- /dev/null +++ b/packages/twenty-ui/src/input/components/SearchInput.module.scss @@ -0,0 +1,55 @@ +.wrapper { + align-items: center; + display: flex; + gap: var(--t-spacing-2); + width: 100%; +} + +.inputContainer { + align-items: center; + background-color: var(--t-background-transparent-lighter); + border: 1px solid var(--t-border-color-medium); + border-radius: var(--t-border-radius-sm); + box-sizing: border-box; + display: flex; + flex: 1; + gap: var(--t-spacing-1); + height: 32px; + padding: 0 var(--t-spacing-2); +} + +.inputContainer:focus-within { + border-color: var(--t-color-blue); +} + +.iconContainer { + align-items: center; + color: var(--t-font-color-light); + display: flex; + justify-content: center; +} + +.iconContainer[data-focused] { + color: var(--t-font-color-secondary); +} + +.input { + background: transparent; + border: none; + color: var(--t-font-color-primary); + flex: 1; + font-family: var(--t-font-family); + font-size: var(--t-font-size-md); + font-weight: var(--t-font-weight-regular); + outline: none; + width: 100%; +} + +.input::placeholder { + color: var(--t-font-color-light); + font-weight: var(--t-font-weight-medium); +} + +.input:disabled { + color: var(--t-font-color-tertiary); +} diff --git a/packages/twenty-ui/src/input/components/SearchInput.module.scss.d.ts b/packages/twenty-ui/src/input/components/SearchInput.module.scss.d.ts new file mode 100644 index 0000000000..cdc1f41b07 --- /dev/null +++ b/packages/twenty-ui/src/input/components/SearchInput.module.scss.d.ts @@ -0,0 +1,7 @@ +declare const classNames: { + readonly wrapper: 'wrapper'; + readonly inputContainer: 'inputContainer'; + readonly iconContainer: 'iconContainer'; + readonly input: 'input'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/input/components/SearchInput.tsx b/packages/twenty-ui/src/input/components/SearchInput.tsx new file mode 100644 index 0000000000..55c7a3f71f --- /dev/null +++ b/packages/twenty-ui/src/input/components/SearchInput.tsx @@ -0,0 +1,58 @@ +import { Input } from '@base-ui/react/input'; +import { clsx } from 'clsx'; +import { type ReactNode, useContext, useState } from 'react'; + +import { IconFilter, IconSearch } from '@ui/display'; +import { IconButton } from '@ui/input/button/components/IconButton'; +import { ThemeContext } from '@ui/theme-constants'; + +import styles from './SearchInput.module.scss'; + +export type SearchInputProps = { + value: string; + onChange: (value: string) => void; + placeholder?: string; + filterDropdown?: (filterButton: ReactNode) => ReactNode; + autoFocus?: boolean; + disabled?: boolean; + className?: string; +}; + +export const SearchInput = ({ + value, + onChange, + placeholder, + filterDropdown, + autoFocus, + disabled, + className, +}: SearchInputProps) => { + const { theme } = useContext(ThemeContext); + const [isFocused, setIsFocused] = useState(false); + + const filterButton = ; + + return ( +
+
+
+ +
+ onChange(newValue)} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + placeholder={placeholder} + autoFocus={autoFocus} + disabled={disabled} + /> +
+ {filterDropdown && filterDropdown(filterButton)} +
+ ); +}; diff --git a/packages/twenty-ui/src/input/components/Toggle.stories.tsx b/packages/twenty-ui/src/input/components/Toggle.stories.tsx deleted file mode 100644 index e6d4dddb70..0000000000 --- a/packages/twenty-ui/src/input/components/Toggle.stories.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { type Meta, type StoryObj } from '@storybook/react-vite'; -import { useState } from 'react'; - -import { ThemeProvider } from '@new-ui/theme-constants'; - -import { Toggle } from './Toggle'; - -const meta: Meta = { - title: 'Input/Toggle', - component: Toggle, - args: { toggleSize: 'medium', 'aria-label': 'Example toggle' }, -}; - -export default meta; - -type Story = StoryObj; - -export const On: Story = { args: { value: true } }; - -export const Off: Story = { args: { value: false } }; - -export const Small: Story = { args: { value: true, toggleSize: 'small' } }; - -export const CustomColor: Story = { - args: { value: true, color: 'color(display-p3 0.2 0.7 0.4)' }, -}; - -export const Disabled: Story = { args: { value: true, disabled: true } }; - -const ControlledToggle = () => { - const [value, setValue] = useState(false); - return ( - - ); -}; - -export const Interactive: Story = { - render: () => , -}; - -export const Dark: Story = { - args: { value: true }, - decorators: [ - (Story) => ( - - - - ), - ], -}; diff --git a/packages/twenty-ui/src/input/components/__stories__/Checkbox.stories.tsx b/packages/twenty-ui/src/input/components/__stories__/Checkbox.stories.tsx new file mode 100644 index 0000000000..efdab51fe1 --- /dev/null +++ b/packages/twenty-ui/src/input/components/__stories__/Checkbox.stories.tsx @@ -0,0 +1,101 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { + CatalogDecorator, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; + +import { + Checkbox, + CheckboxAccent, + CheckboxShape, + CheckboxSize, + CheckboxVariant, +} from '../Checkbox'; + +const meta: Meta = { + title: 'UI/Input/Checkbox/Checkbox', + component: Checkbox, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + checked: false, + indeterminate: false, + hoverable: false, + disabled: false, + variant: CheckboxVariant.Primary, + size: CheckboxSize.Small, + shape: CheckboxShape.Squared, + accent: CheckboxAccent.Blue, + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: {}, + argTypes: { + variant: { control: false }, + size: { control: false }, + indeterminate: { control: false }, + checked: { control: false }, + hoverable: { control: false }, + shape: { control: false }, + accent: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + catalog: { + dimensions: [ + { + name: 'state', + values: ['disabled', 'unchecked', 'checked', 'indeterminate'], + props: (state: string) => { + if (state === 'disabled') { + return { disabled: true }; + } + if (state === 'checked') { + return { checked: true }; + } + if (state === 'indeterminate') { + return { indeterminate: true }; + } + return {}; + }, + }, + { + name: 'shape', + values: Object.values(CheckboxShape), + props: (shape: CheckboxShape) => ({ shape }), + }, + { + name: 'isHoverable', + values: ['default', 'hoverable'], + props: (isHoverable: string) => { + if (isHoverable === 'hoverable') { + return { hoverable: true }; + } + return {}; + }, + }, + { + name: 'size', + values: Object.values(CheckboxSize), + props: (size: CheckboxSize) => ({ size }), + }, + { + name: 'accent', + values: Object.values(CheckboxAccent), + props: (accent: CheckboxAccent) => ({ accent }), + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/input/components/__stories__/Radio.stories.tsx b/packages/twenty-ui/src/input/components/__stories__/Radio.stories.tsx new file mode 100644 index 0000000000..763587b74e --- /dev/null +++ b/packages/twenty-ui/src/input/components/__stories__/Radio.stories.tsx @@ -0,0 +1,66 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { + CatalogDecorator, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; + +import { LabelPosition, Radio, RadioSize } from '../Radio'; + +const meta: Meta = { + title: 'UI/Input/Radio/Radio', + component: Radio, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'Radio', + checked: false, + disabled: false, + size: RadioSize.Small, + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: { + label: 'Radio', + }, + argTypes: { + size: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + catalog: { + dimensions: [ + { + name: 'checked', + values: [false, true], + props: (checked: boolean) => ({ checked }), + }, + { + name: 'disabled', + values: [false, true], + props: (disabled: boolean) => ({ disabled }), + }, + { + name: 'size', + values: Object.values(RadioSize), + props: (size: RadioSize) => ({ size }), + }, + { + name: 'labelPosition', + values: Object.values(LabelPosition), + props: (labelPosition: LabelPosition) => ({ + labelPosition, + }), + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/input/components/__stories__/SearchInput.stories.tsx b/packages/twenty-ui/src/input/components/__stories__/SearchInput.stories.tsx new file mode 100644 index 0000000000..e454a75fc0 --- /dev/null +++ b/packages/twenty-ui/src/input/components/__stories__/SearchInput.stories.tsx @@ -0,0 +1,54 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { ComponentDecorator } from '@ui/testing'; +import { useState } from 'react'; + +import { SearchInput } from '../SearchInput'; + +type InteractiveSearchInputProps = { + placeholder?: string; + disabled?: boolean; + autoFocus?: boolean; + className?: string; +}; + +const InteractiveSearchInput = ({ + placeholder, + disabled, + autoFocus, + className, +}: InteractiveSearchInputProps) => { + const [value, setValue] = useState(''); + + return ( + + ); +}; + +const meta: Meta = { + title: 'UI/Input/SearchInput', + component: InteractiveSearchInput, + decorators: [ComponentDecorator], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + placeholder: 'Search...', + }, +}; + +export const Disabled: Story = { + args: { + placeholder: 'Search...', + disabled: true, + }, +}; diff --git a/packages/twenty-ui/src/input/components/__stories__/Toggle.stories.tsx b/packages/twenty-ui/src/input/components/__stories__/Toggle.stories.tsx new file mode 100644 index 0000000000..6a0c708d00 --- /dev/null +++ b/packages/twenty-ui/src/input/components/__stories__/Toggle.stories.tsx @@ -0,0 +1,102 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { + CatalogDecorator, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; +import { themeCssVariables } from '@ui/theme-constants'; +import { useState } from 'react'; + +import { Toggle, type ToggleSize } from '../Toggle'; + +type InteractiveToggleProps = { + initialValue?: boolean; + toggleSize?: ToggleSize; + color?: string; + disabled?: boolean; +}; + +const InteractiveToggle = ({ + initialValue = false, + toggleSize = 'medium', + color, + disabled, +}: InteractiveToggleProps) => { + const [value, setValue] = useState(initialValue); + + return ( + + ); +}; + +const meta: Meta = { + title: 'UI/Input/Toggle/Toggle', + component: InteractiveToggle, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + initialValue: false, + disabled: false, + toggleSize: 'medium', + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: {}, + argTypes: { + toggleSize: { control: false }, + initialValue: { control: false }, + disabled: { control: false }, + color: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + catalog: { + dimensions: [ + { + name: 'state', + values: ['disabled', 'off', 'on'], + props: (state: string) => { + if (state === 'disabled') { + return { disabled: true, initialValue: false }; + } + if (state === 'on') { + return { initialValue: true }; + } + return { initialValue: false }; + }, + }, + { + name: 'size', + values: ['small', 'medium'] satisfies ToggleSize[], + props: (toggleSize: ToggleSize) => ({ toggleSize }), + }, + { + name: 'color', + values: ['default', 'custom color'], + props: (color: string) => { + if (color === 'default') { + return {}; + } + return { color: themeCssVariables.color.yellow }; + }, + }, + ], + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/input/index.ts b/packages/twenty-ui/src/input/index.ts index b4e27c1fae..0e148b1653 100644 --- a/packages/twenty-ui/src/input/index.ts +++ b/packages/twenty-ui/src/input/index.ts @@ -7,5 +7,102 @@ * |___/ */ +export type { AnimatedButtonProps } from './button/components/AnimatedButton'; +export { AnimatedButton } from './button/components/AnimatedButton'; +export type { AnimatedLightIconButtonProps } from './button/components/AnimatedLightIconButton'; +export { AnimatedLightIconButton } from './button/components/AnimatedLightIconButton'; +export type { + ButtonSize, + ButtonPosition, + ButtonVariant, + ButtonAccent, + ButtonProps, +} from './button/components/Button/Button'; +export { Button } from './button/components/Button/Button'; +export { baseTransitionTiming } from './button/components/Button/constant'; +export type { ButtonGroupProps } from './button/components/ButtonGroup'; +export { ButtonGroup } from './button/components/ButtonGroup'; +export { ColorPickerButton } from './button/components/ColorPickerButton'; +export type { + FloatingButtonSize, + FloatingButtonPosition, + FloatingButtonProps, +} from './button/components/FloatingButton'; +export { FloatingButton } from './button/components/FloatingButton'; +export type { FloatingButtonGroupProps } from './button/components/FloatingButtonGroup'; +export { FloatingButtonGroup } from './button/components/FloatingButtonGroup'; +export type { + FloatingIconButtonSize, + FloatingIconButtonPosition, + FloatingIconButtonProps, +} from './button/components/FloatingIconButton'; +export { FloatingIconButton } from './button/components/FloatingIconButton'; +export type { FloatingIconButtonGroupProps } from './button/components/FloatingIconButtonGroup'; +export { FloatingIconButtonGroup } from './button/components/FloatingIconButtonGroup'; +export type { + IconButtonSize, + IconButtonPosition, + IconButtonVariant, + IconButtonAccent, + IconButtonProps, +} from './button/components/IconButton'; +export { IconButton } from './button/components/IconButton'; +export type { IconButtonGroupProps } from './button/components/IconButtonGroup'; +export { IconButtonGroup } from './button/components/IconButtonGroup'; +export type { InsideButtonProps } from './button/components/InsideButton'; +export { InsideButton } from './button/components/InsideButton'; +export type { + LightButtonAccent, + LightButtonProps, +} from './button/components/LightButton'; +export { LightButton } from './button/components/LightButton'; +export type { + LightIconButtonAccent, + LightIconButtonSize, + LightIconButtonProps, +} from './button/components/LightIconButton'; +export { LightIconButton } from './button/components/LightIconButton'; +export type { LightIconButtonGroupProps } from './button/components/LightIconButtonGroup'; +export { LightIconButtonGroup } from './button/components/LightIconButtonGroup'; +export type { MainButtonVariant } from './button/components/MainButton'; +export { MainButton } from './button/components/MainButton'; +export type { RoundedIconButtonSize } from './button/components/RoundedIconButton'; +export { RoundedIconButton } from './button/components/RoundedIconButton'; +export { + StyledTabButton, + StyledTabContainer, + StyledTabHover, +} from './button/components/TabButton/internals/components/StyledTabBase'; +export type { TabContentProps } from './button/components/TabButton/internals/components/TabContent'; +export { TabContent } from './button/components/TabButton/internals/components/TabContent'; +export { TabButton } from './button/components/TabButton/TabButton'; +export { CodeEditor } from './code-editor/components/CodeEditor'; +export type { CoreEditorHeaderProps } from './code-editor/components/CodeEditorHeader'; +export { CoreEditorHeader } from './code-editor/components/CodeEditorHeader'; +export { BASE_CODE_EDITOR_THEME_ID } from './code-editor/constants/BaseCodeEditorThemeId'; +export { getBaseCodeEditorTheme } from './code-editor/theme/utils/getBaseCodeEditorTheme'; +export type { + ColorSchemeSegmentProps, + ColorSchemeCardProps, +} from './color-scheme/components/ColorSchemeCard'; +export { ColorSchemeCard } from './color-scheme/components/ColorSchemeCard'; +export type { ColorSchemePickerProps } from './color-scheme/components/ColorSchemePicker'; +export { ColorSchemePicker } from './color-scheme/components/ColorSchemePicker'; +export { CardPicker } from './components/CardPicker'; +export { + CheckboxVariant, + CheckboxShape, + CheckboxSize, + CheckboxAccent, + Checkbox, +} from './components/Checkbox'; +export { IconListViewGrip } from './components/IconListViewGrip'; +export type { RadioProps } from './components/Radio'; +export { RadioSize, LabelPosition, Radio } from './components/Radio'; +export { RadioGroup } from './components/RadioGroup'; +export type { SearchInputProps } from './components/SearchInput'; +export { SearchInput } from './components/SearchInput'; export type { ToggleSize, ToggleProps } from './components/Toggle'; export { Toggle } from './components/Toggle'; +export type { ColorScheme } from './types/ColorScheme'; +export type { SelectOption } from './types/SelectOption'; diff --git a/packages/twenty-ui/src/input/types/ColorScheme.ts b/packages/twenty-ui/src/input/types/ColorScheme.ts new file mode 100644 index 0000000000..0a5f3723d7 --- /dev/null +++ b/packages/twenty-ui/src/input/types/ColorScheme.ts @@ -0,0 +1 @@ +export type ColorScheme = 'Dark' | 'Light' | 'System'; diff --git a/packages/twenty-ui/src/input/types/SelectOption.ts b/packages/twenty-ui/src/input/types/SelectOption.ts new file mode 100644 index 0000000000..e2a5190f18 --- /dev/null +++ b/packages/twenty-ui/src/input/types/SelectOption.ts @@ -0,0 +1,15 @@ +import { type IconComponent } from '@ui/display'; +import { type ThemeColor } from '@ui/theme'; + +export type SelectOption< + Value extends string | number | boolean | null = string, +> = { + Icon?: IconComponent | null; + iconThemeColor?: ThemeColor | null; + label: string; + fullLabel?: string; + value: Value; + disabled?: boolean; + color?: ThemeColor | 'transparent'; + contextualText?: string; +}; diff --git a/packages/twenty-ui/src/json-visualizer/__stories__/JsonTree.stories.tsx b/packages/twenty-ui/src/json-visualizer/__stories__/JsonTree.stories.tsx new file mode 100644 index 0000000000..a33e1715bc --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/__stories__/JsonTree.stories.tsx @@ -0,0 +1,661 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { JsonTree } from '@ui/json-visualizer/components/JsonTree'; +import { isTwoFirstDepths } from '@ui/json-visualizer/utils/isTwoFirstDepths'; +import { + expect, + fn, + userEvent, + waitFor, + waitForElementToBeRemoved, + within, +} from 'storybook/test'; + +const meta: Meta = { + title: 'UI/JsonVisualizer/JsonTree', + component: JsonTree, + args: { + shouldExpandNodeInitially: () => true, + emptyArrayLabel: 'Empty Array', + emptyObjectLabel: 'Empty Object', + emptyStringLabel: '[empty string]', + arrowButtonCollapsedLabel: 'Expand', + arrowButtonExpandedLabel: 'Collapse', + }, + argTypes: {}, +}; + +export default meta; + +type Story = StoryObj; + +export const String: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: 'Hello', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const node = await canvas.findByText('Hello'); + + expect(node).toBeVisible(); + }, +}; + +export const StringWithSpecialCharacters: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: 'Merry \n Christmas \t 🎄', + onNodeValueClick: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const node = await canvas.findByText('Merry Christmas 🎄'); + + await userEvent.click(node); + + await waitFor(() => { + expect(args.onNodeValueClick).toHaveBeenCalledWith( + 'Merry \n Christmas \t 🎄', + ); + }); + }, +}; + +export const Number: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: 42, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const node = await canvas.findByText('42'); + + expect(node).toBeVisible(); + }, +}; + +export const Boolean: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const node = await canvas.findByText('true'); + + expect(node).toBeVisible(); + }, +}; + +export const Null: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: null, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const node = await canvas.findByText('null'); + + expect(node).toBeVisible(); + }, +}; + +export const ArraySimple: Story = { + args: { + value: [1, 2, 3], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const node = await canvas.findByText('3'); + + expect(node).toBeVisible(); + }, +}; + +export const ArrayEmpty: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: [], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const emptyState = await canvas.findByText('Empty Array'); + + expect(emptyState).toBeVisible(); + }, +}; + +export const ArrayNested: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: [1, 2, ['a', 'b', 'c'], 3], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const nestedArrayElements = await canvas.findByText('[3]'); + + expect(nestedArrayElements).toBeVisible(); + }, +}; + +export const ArrayNestedEmpty: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: [1, 2, [], 3], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const nestedArrayElements = await canvas.findByText('[0]'); + + expect(nestedArrayElements).toBeVisible(); + + const emptyState = await canvas.findByText('Empty Array'); + + expect(emptyState).toBeVisible(); + }, +}; + +export const ArrayWithObjects: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: [ + { + name: 'John Doe', + age: 30, + }, + { + name: 'John Dowl', + age: 42, + }, + ], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const nestedObjectItemsCounts = await canvas.findAllByText('{2}'); + + expect(nestedObjectItemsCounts).toHaveLength(2); + }, +}; + +export const ObjectSimple: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: { + name: 'John Doe', + age: 30, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const name = await canvas.findByText('John Doe'); + expect(name).toBeVisible(); + + const age = await canvas.findByText('30'); + expect(age).toBeVisible(); + }, +}; + +export const ObjectEmpty: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: {}, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const emptyState = await canvas.findByText('Empty Object'); + + expect(emptyState).toBeVisible(); + }, +}; + +export const ObjectNested: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: { + person: { + name: 'John Doe', + address: { + street: '123 Main St', + city: 'New York', + }, + }, + isActive: true, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const nestedObjectItemsCounts = await canvas.findAllByText('{2}'); + + expect(nestedObjectItemsCounts).toHaveLength(2); + }, +}; + +export const ObjectNestedEmpty: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: { + person: {}, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const nestedObjectItemsCount = await canvas.findByText('{0}'); + + expect(nestedObjectItemsCount).toBeVisible(); + + const emptyState = await canvas.findByText('Empty Object'); + + expect(emptyState).toBeVisible(); + }, +}; + +export const ObjectWithArray: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: { + users: [ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' }, + ], + settings: { + theme: 'dark', + notifications: true, + }, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const nestedArrayCount = await canvas.findByText('[2]'); + expect(nestedArrayCount).toBeVisible(); + + const nestedObjectCounts = await canvas.findAllByText('{2}'); + expect(nestedObjectCounts).toHaveLength(3); + }, +}; + +export const NestedElementCanBeCollapsed: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: { + person: { + name: 'John Doe', + age: 12, + }, + isActive: true, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const toggleButton = await canvas.findByRole('button', { + name: 'Collapse', + }); + + const ageElement = canvas.getByText('age'); + + await Promise.all([ + waitForElementToBeRemoved(ageElement), + + userEvent.click(toggleButton), + ]); + + expect(toggleButton).toHaveTextContent('Expand'); + }, +}; + +export const ExpandingElementExpandsAllItsDescendants: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: { + person: { + name: 'John Doe', + address: { + street: '123 Main St', + city: 'New York', + country: { + name: 'USA', + code: 'US', + }, + }, + }, + isActive: true, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + { + const allCollapseButtons = await canvas.findAllByRole('button', { + name: 'Collapse', + }); + + expect(allCollapseButtons).toHaveLength(3); + + for (const collapseButton of allCollapseButtons.reverse()) { + await userEvent.click(collapseButton); + } + } + + const rootExpandButton = await canvas.findByRole('button', { + name: 'Expand', + }); + + await userEvent.click(rootExpandButton); + + { + const allCollapseButtons = await canvas.findAllByRole('button', { + name: 'Collapse', + }); + + expect(allCollapseButtons).toHaveLength(3); + } + }, +}; + +export const ExpandTwoFirstDepths: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: { + person: { + name: 'John Doe', + address: { + street: '123 Main St', + city: 'New York', + country: { + name: 'USA', + code: 'US', + }, + }, + }, + isActive: true, + }, + shouldExpandNodeInitially: isTwoFirstDepths, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const nameElement = await canvas.findByText('name'); + expect(nameElement).toBeVisible(); + + const addressElement = await canvas.findByText('address'); + expect(addressElement).toBeVisible(); + + const streetElement = canvas.queryByText('street'); + expect(streetElement).not.toBeInTheDocument(); + + const countrCodeElement = canvas.queryByText('code'); + expect(countrCodeElement).not.toBeInTheDocument(); + }, +}; + +export const ReallyDeepNestedObject: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: { + a: { + b: { + c: { + d: { + e: { + f: { + g: { + h: { + i: { + j: { + k: { + l: { + m: { + n: { + o: { + p: { + q: { + r: { + s: { + t: { + u: { + v: { + w: { + x: { + y: { + z: { + end: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + bis: { + c: { + d: { + e: { + f: { + g: { + h: { + i: { + j: { + k: { + l: { + m: { + n: { + o: { + p: { + q: { + r: { + s: { + t: { + u: { + v: { + w: { + x: { + y: { + z: { + end: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const finalNodes = await canvas.findAllByText('end'); + + expect(finalNodes).toHaveLength(2); + expect(finalNodes[0]).toBeVisible(); + expect(finalNodes[1]).toBeVisible(); + }, +}; + +export const LongText: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: { + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum iaculis est tincidunt, sagittis neque vitae, sodales purus.': + 'Ut lobortis ultricies purus, sit amet porta eros. Suspendisse efficitur quam vitae diam imperdiet feugiat. Etiam vel bibendum elit.', + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const label = await canvas.findByText( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum iaculis est tincidunt, sagittis neque vitae, sodales purus.', + ); + + expect(label).toBeVisible(); + + const value = await canvas.findByText( + 'Ut lobortis ultricies purus, sit amet porta eros. Suspendisse efficitur quam vitae diam imperdiet feugiat. Etiam vel bibendum elit.', + ); + + expect(value).toBeVisible(); + }, +}; + +export const BlueHighlighting: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: { + name: 'John Doe', + age: 30, + }, + getNodeHighlighting: () => 'blue', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const ageElement = await canvas.findByText('age'); + expect(ageElement).toBeVisible(); + }, +}; + +export const PartialBlueHighlighting: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: { + name: 'John Doe', + age: 30, + address: { + city: 'Paris', + }, + }, + getNodeHighlighting: (keyPath: string) => + keyPath === 'address' ? 'partial-blue' : undefined, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const ageElement = await canvas.findByText('age'); + expect(ageElement).toBeVisible(); + }, +}; + +export const RedHighlighting: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: { + name: 'John Doe', + age: 30, + address: { + city: 'Paris', + }, + }, + getNodeHighlighting: () => 'red', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const ageElement = await canvas.findByText('age'); + expect(ageElement).toBeVisible(); + }, +}; + +export const CopyJsonNodeValue: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + value: { + name: 'John Doe', + age: 30, + }, + onNodeValueClick: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const nameValue = await canvas.findByText('John Doe'); + + await userEvent.click(nameValue); + + await waitFor(() => { + expect(args.onNodeValueClick).toHaveBeenCalledWith('John Doe'); + }); + + const ageValue = await canvas.findByText('30'); + + await userEvent.click(ageValue); + + await waitFor(() => { + expect(args.onNodeValueClick).toHaveBeenCalledWith('30'); + }); + }, +}; diff --git a/packages/twenty-ui/src/json-visualizer/components/JsonArrayNode.tsx b/packages/twenty-ui/src/json-visualizer/components/JsonArrayNode.tsx new file mode 100644 index 0000000000..14278f0455 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/JsonArrayNode.tsx @@ -0,0 +1,38 @@ +import { IconBrackets } from '@ui/display'; +import { JsonNestedNode } from '@ui/json-visualizer/components/JsonNestedNode'; +import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow'; +import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting'; +import { type JsonArray } from 'type-fest'; + +export const JsonArrayNode = ({ + label, + value, + depth, + keyPath, + highlighting, +}: { + label?: string; + value: JsonArray; + depth: number; + keyPath: string; + highlighting: JsonNodeHighlighting | undefined; +}) => { + const { emptyArrayLabel } = useJsonTreeContextOrThrow(); + + return ( + ({ + id: key, + label: String(key), + value, + }))} + renderElementsCount={(count) => `[${count}]`} + label={label} + Icon={IconBrackets} + depth={depth} + emptyElementsText={emptyArrayLabel} + keyPath={keyPath} + highlighting={highlighting} + /> + ); +}; diff --git a/packages/twenty-ui/src/json-visualizer/components/JsonNestedNode.module.scss b/packages/twenty-ui/src/json-visualizer/components/JsonNestedNode.module.scss new file mode 100644 index 0000000000..f51bfc0e08 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/JsonNestedNode.module.scss @@ -0,0 +1,50 @@ +.container { + display: grid; + list-style-type: none; +} + +.labelContainer { + display: flex; + align-items: center; + gap: var(--t-spacing-2); +} + +.elementsCount { + color: var(--t-font-color-tertiary); +} + +.elementsCountRed { + color: var(--t-font-color-danger); +} + +// Base UI sets --collapsible-panel-height to a pixel value during +// open/close transitions and back to auto once fully open, so nested +// nodes can still resize their ancestors. +.panel { + height: var(--collapsible-panel-height); + overflow: clip; + transition: + height 0.3s ease, + opacity 0.3s ease; + + &[data-starting-style], + &[data-ending-style] { + height: 0; + opacity: 0; + } +} + +.list { + margin: 0; + padding: 0; + display: grid; + row-gap: var(--t-spacing-2); +} + +.nested { + padding-left: var(--t-spacing-8); + + > :first-of-type { + margin-top: var(--t-spacing-2); + } +} diff --git a/packages/twenty-ui/src/json-visualizer/components/JsonNestedNode.module.scss.d.ts b/packages/twenty-ui/src/json-visualizer/components/JsonNestedNode.module.scss.d.ts new file mode 100644 index 0000000000..6835d36a26 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/JsonNestedNode.module.scss.d.ts @@ -0,0 +1,10 @@ +declare const classNames: { + readonly container: 'container'; + readonly labelContainer: 'labelContainer'; + readonly elementsCount: 'elementsCount'; + readonly elementsCountRed: 'elementsCountRed'; + readonly panel: 'panel'; + readonly list: 'list'; + readonly nested: 'nested'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/json-visualizer/components/JsonNestedNode.tsx b/packages/twenty-ui/src/json-visualizer/components/JsonNestedNode.tsx new file mode 100644 index 0000000000..442cf5ea13 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/JsonNestedNode.tsx @@ -0,0 +1,119 @@ +import { Collapsible } from '@base-ui/react/collapsible'; +import { isNonEmptyString } from '@sniptt/guards'; +import { clsx } from 'clsx'; +import { useState } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; +import { type JsonValue } from 'type-fest'; + +import { type IconComponent } from '@ui/display'; +import { JsonArrow } from '@ui/json-visualizer/components/internal/JsonArrow'; +import { JsonNodeLabel } from '@ui/json-visualizer/components/internal/JsonNodeLabel'; +import { JsonNodeValue } from '@ui/json-visualizer/components/internal/JsonNodeValue'; +import { JsonNode } from '@ui/json-visualizer/components/JsonNode'; +import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow'; +import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting'; + +import styles from './JsonNestedNode.module.scss'; + +export const JsonNestedNode = ({ + label, + Icon, + elements, + renderElementsCount, + emptyElementsText, + depth, + keyPath, + highlighting, +}: { + label?: string; + Icon: IconComponent; + elements: Array<{ id: string | number; label: string; value: JsonValue }>; + renderElementsCount?: (count: number) => string; + emptyElementsText: string; + depth: number; + keyPath: string; + highlighting?: JsonNodeHighlighting | undefined; +}) => { + const { shouldExpandNodeInitially } = useJsonTreeContextOrThrow(); + + const hideRoot = !isDefined(label); + + const [isOpen, setIsOpen] = useState( + shouldExpandNodeInitially({ keyPath, depth }), + ); + + const renderedChildren = ( +
    0 && styles.nested)}> + {elements.length === 0 ? ( + + ) : ( + elements.map(({ id, label, value }) => { + const nextKeyPath = isNonEmptyString(keyPath) + ? `${keyPath}.${id}` + : String(id); + + return ( + + ); + }) + )} +
+ ); + + const handleArrowClick = () => { + setIsOpen(!isOpen); + }; + + if (hideRoot) { + return
  • {renderedChildren}
  • ; + } + + return ( + } + > +
    + + + + + {renderElementsCount && ( + + {renderElementsCount(elements.length)} + + )} +
    + + + {renderedChildren} + +
    + ); +}; diff --git a/packages/twenty-ui/src/json-visualizer/components/JsonNode.tsx b/packages/twenty-ui/src/json-visualizer/components/JsonNode.tsx new file mode 100644 index 0000000000..427a76363e --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/JsonNode.tsx @@ -0,0 +1,101 @@ +import { + isBoolean, + isNonEmptyString, + isNumber, + isString, +} from '@sniptt/guards'; +import { + IconCheckbox, + IconCircleOff, + IconNumber9, + IconTypography, +} from '@ui/display'; +import { JsonArrayNode } from '@ui/json-visualizer/components/JsonArrayNode'; +import { JsonObjectNode } from '@ui/json-visualizer/components/JsonObjectNode'; +import { JsonValueNode } from '@ui/json-visualizer/components/JsonValueNode'; +import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow'; +import { isArray } from '@ui/json-visualizer/utils/isArray'; +import { isDefined } from '@ui/utilities/utils/isDefined'; +import { type JsonValue } from 'type-fest'; + +export const JsonNode = ({ + label, + value, + depth, + keyPath, +}: { + label?: string; + value: JsonValue; + depth: number; + keyPath: string; +}) => { + const { getNodeHighlighting, emptyStringLabel } = useJsonTreeContextOrThrow(); + + const highlighting = getNodeHighlighting?.(keyPath); + + if (!isDefined(value)) { + return ( + + ); + } + + if (isString(value)) { + return ( + + ); + } + + if (isNumber(value)) { + return ( + + ); + } + + if (isBoolean(value)) { + return ( + + ); + } + + if (isArray(value)) { + return ( + + ); + } + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/json-visualizer/components/JsonObjectNode.tsx b/packages/twenty-ui/src/json-visualizer/components/JsonObjectNode.tsx new file mode 100644 index 0000000000..e83919d051 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/JsonObjectNode.tsx @@ -0,0 +1,38 @@ +import { IconCube } from '@ui/display'; +import { JsonNestedNode } from '@ui/json-visualizer/components/JsonNestedNode'; +import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow'; +import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting'; +import { type JsonObject } from 'type-fest'; + +export const JsonObjectNode = ({ + label, + value, + depth, + keyPath, + highlighting, +}: { + label?: string; + value: JsonObject; + depth: number; + keyPath: string; + highlighting: JsonNodeHighlighting | undefined; +}) => { + const { emptyObjectLabel } = useJsonTreeContextOrThrow(); + + return ( + ({ + id: key, + label: key, + value, + }))} + renderElementsCount={(count) => `{${count}}`} + label={label} + Icon={IconCube} + depth={depth} + emptyElementsText={emptyObjectLabel} + keyPath={keyPath} + highlighting={highlighting} + /> + ); +}; diff --git a/packages/twenty-ui/src/json-visualizer/components/JsonTree.tsx b/packages/twenty-ui/src/json-visualizer/components/JsonTree.tsx new file mode 100644 index 0000000000..c0eb9b0b2b --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/JsonTree.tsx @@ -0,0 +1,49 @@ +import { JsonList } from '@ui/json-visualizer/components/internal/JsonList'; +import { JsonNode } from '@ui/json-visualizer/components/JsonNode'; +import { JsonTreeContextProvider } from '@ui/json-visualizer/components/JsonTreeContextProvider'; +import { type ShouldExpandNodeInitiallyProps } from '@ui/json-visualizer/contexts/JsonTreeContext'; +import { type GetJsonNodeHighlighting } from '@ui/json-visualizer/types/GetJsonNodeHighlighting'; +import { type JsonValue } from 'type-fest'; + +export const JsonTree = ({ + value, + getNodeHighlighting, + shouldExpandNodeInitially, + emptyArrayLabel, + emptyObjectLabel, + emptyStringLabel, + arrowButtonCollapsedLabel, + arrowButtonExpandedLabel, + onNodeValueClick, +}: { + value: JsonValue; + getNodeHighlighting?: GetJsonNodeHighlighting; + shouldExpandNodeInitially: ( + params: ShouldExpandNodeInitiallyProps, + ) => boolean; + emptyArrayLabel: string; + emptyObjectLabel: string; + emptyStringLabel: string; + arrowButtonCollapsedLabel: string; + arrowButtonExpandedLabel: string; + onNodeValueClick?: (valueAsString: string) => void; +}) => { + return ( + + + + + + ); +}; diff --git a/packages/twenty-ui/src/json-visualizer/components/JsonTreeContextProvider.tsx b/packages/twenty-ui/src/json-visualizer/components/JsonTreeContextProvider.tsx new file mode 100644 index 0000000000..61cb229a5d --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/JsonTreeContextProvider.tsx @@ -0,0 +1,18 @@ +import { + JsonTreeContext, + type JsonTreeContextType, +} from '@ui/json-visualizer/contexts/JsonTreeContext'; + +export const JsonTreeContextProvider = ({ + value, + children, +}: { + value: JsonTreeContextType; + children: React.ReactNode; +}) => { + return ( + + {children} + + ); +}; diff --git a/packages/twenty-ui/src/json-visualizer/components/JsonValueNode.module.scss b/packages/twenty-ui/src/json-visualizer/components/JsonValueNode.module.scss new file mode 100644 index 0000000000..702270ff37 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/JsonValueNode.module.scss @@ -0,0 +1,7 @@ +.listItem { + align-items: center; + column-gap: var(--t-spacing-2); + display: flex; + list-style-type: none; + white-space: nowrap; +} diff --git a/packages/twenty-ui/src/json-visualizer/components/JsonValueNode.module.scss.d.ts b/packages/twenty-ui/src/json-visualizer/components/JsonValueNode.module.scss.d.ts new file mode 100644 index 0000000000..45f8cd8e1e --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/JsonValueNode.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly listItem: 'listItem'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/json-visualizer/components/JsonValueNode.tsx b/packages/twenty-ui/src/json-visualizer/components/JsonValueNode.tsx new file mode 100644 index 0000000000..fc5bed8c7b --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/JsonValueNode.tsx @@ -0,0 +1,39 @@ +import { type IconComponent } from '@ui/display'; +import { JsonNodeLabel } from '@ui/json-visualizer/components/internal/JsonNodeLabel'; +import { JsonNodeValue } from '@ui/json-visualizer/components/internal/JsonNodeValue'; +import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting'; + +import styles from './JsonValueNode.module.scss'; + +type JsonValueNodeProps = { + valueAsString: string; + highlighting: JsonNodeHighlighting | undefined; +} & ( + | { + label: string; + Icon: IconComponent; + } + | { + label?: never; + Icon?: unknown; + } +); + +export const JsonValueNode = (props: JsonValueNodeProps) => { + return ( +
  • + {props.label && ( + + )} + + +
  • + ); +}; diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonArrow.module.scss b/packages/twenty-ui/src/json-visualizer/components/internal/JsonArrow.module.scss new file mode 100644 index 0000000000..3480e0bb4d --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonArrow.module.scss @@ -0,0 +1,29 @@ +.button { + align-items: center; + background-color: var(--t-background-transparent-lighter); + border-color: var(--t-border-color-medium); + border-radius: var(--t-border-radius-sm); + border-style: solid; + border-width: 1px; + display: flex; + justify-content: center; + padding-inline: var(--t-spacing-1); + height: 24px; + width: 24px; + box-sizing: border-box; + cursor: pointer; +} + +.red { + background-color: var(--t-background-danger); + border-color: var(--t-border-color-danger); +} + +.chevron { + transform: rotate(-90deg); + transition: transform 0.3s ease; + + &[data-open] { + transform: rotate(0deg); + } +} diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonArrow.module.scss.d.ts b/packages/twenty-ui/src/json-visualizer/components/internal/JsonArrow.module.scss.d.ts new file mode 100644 index 0000000000..a4ec7bb6dc --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonArrow.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly button: 'button'; + readonly red: 'red'; + readonly chevron: 'chevron'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonArrow.tsx b/packages/twenty-ui/src/json-visualizer/components/internal/JsonArrow.tsx new file mode 100644 index 0000000000..8ce1f32495 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonArrow.tsx @@ -0,0 +1,44 @@ +import { VisibilityHidden } from '@ui/accessibility'; +import { IconChevronDown } from '@ui/display'; +import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow'; +import { ThemeContext, themeCssVariables } from '@ui/theme-constants'; +import { clsx } from 'clsx'; +import { useContext } from 'react'; + +import styles from './JsonArrow.module.scss'; + +export const JsonArrow = ({ + isOpen, + onClick, + variant, +}: { + isOpen: boolean; + onClick: () => void; + variant?: 'blue' | 'red'; +}) => { + const { theme } = useContext(ThemeContext); + const { arrowButtonCollapsedLabel, arrowButtonExpandedLabel } = + useJsonTreeContextOrThrow(); + + const iconColor = + variant === 'blue' + ? themeCssVariables.color.blue + : variant === 'red' + ? themeCssVariables.font.color.danger + : themeCssVariables.font.color.secondary; + + return ( + + ); +}; diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonList.module.scss b/packages/twenty-ui/src/json-visualizer/components/internal/JsonList.module.scss new file mode 100644 index 0000000000..2fe86fecc7 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonList.module.scss @@ -0,0 +1,15 @@ +.list { + margin: 0; + padding: 0; + + display: grid; + row-gap: var(--t-spacing-2); +} + +.nested { + padding-left: var(--t-spacing-8); + + > :first-of-type { + margin-top: var(--t-spacing-2); + } +} diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonList.module.scss.d.ts b/packages/twenty-ui/src/json-visualizer/components/internal/JsonList.module.scss.d.ts new file mode 100644 index 0000000000..03ac0a8b27 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonList.module.scss.d.ts @@ -0,0 +1,5 @@ +declare const classNames: { + readonly list: 'list'; + readonly nested: 'nested'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonList.tsx b/packages/twenty-ui/src/json-visualizer/components/internal/JsonList.tsx new file mode 100644 index 0000000000..18da934624 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonList.tsx @@ -0,0 +1,17 @@ +import { clsx } from 'clsx'; + +import styles from './JsonList.module.scss'; + +export const JsonList = ({ + depth, + className, + children, +}: { + depth: number; + className?: string; + children?: React.ReactNode; +}) => ( +
      0 && styles.nested, className)}> + {children} +
    +); diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonListItem.module.scss b/packages/twenty-ui/src/json-visualizer/components/internal/JsonListItem.module.scss new file mode 100644 index 0000000000..c3e0eeb89e --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonListItem.module.scss @@ -0,0 +1,6 @@ +.listItem { + align-items: center; + display: flex; + list-style-type: none; + white-space: nowrap; +} diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonListItem.module.scss.d.ts b/packages/twenty-ui/src/json-visualizer/components/internal/JsonListItem.module.scss.d.ts new file mode 100644 index 0000000000..45f8cd8e1e --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonListItem.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly listItem: 'listItem'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonListItem.tsx b/packages/twenty-ui/src/json-visualizer/components/internal/JsonListItem.tsx new file mode 100644 index 0000000000..2eb5e37d7c --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonListItem.tsx @@ -0,0 +1,11 @@ +import { clsx } from 'clsx'; + +import styles from './JsonListItem.module.scss'; + +export const JsonListItem = ({ + className, + children, +}: { + className?: string; + children?: React.ReactNode; +}) =>
  • {children}
  • ; diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeLabel.module.scss b/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeLabel.module.scss new file mode 100644 index 0000000000..d577a40e91 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeLabel.module.scss @@ -0,0 +1,34 @@ +.labelContainer { + background-color: var(--t-background-transparent-lighter); + border-color: var(--t-border-color-medium); + color: var(--t-font-color-primary); + border-radius: var(--t-border-radius-sm); + border-style: solid; + border-width: 1px; + column-gap: var(--t-spacing-2); + display: inline-flex; + align-items: center; + height: 24px; + box-sizing: border-box; + font-size: var(--t-font-size-md); + white-space: nowrap; + padding-inline: var(--t-spacing-2); + + > span { + align-items: center; + display: inline-flex; + line-height: 1; + } +} + +.blue { + background-color: var(--t-color-blue3); + border-color: var(--t-color-blue5); + color: var(--t-color-blue); +} + +.red { + background-color: var(--t-background-danger); + border-color: var(--t-border-color-danger); + color: var(--t-font-color-danger); +} diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeLabel.module.scss.d.ts b/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeLabel.module.scss.d.ts new file mode 100644 index 0000000000..d6ec9ae1dd --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeLabel.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly labelContainer: 'labelContainer'; + readonly blue: 'blue'; + readonly red: 'red'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeLabel.tsx b/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeLabel.tsx new file mode 100644 index 0000000000..8e3fbd0457 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeLabel.tsx @@ -0,0 +1,42 @@ +import { type IconComponent } from '@ui/display'; +import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting'; +import { ThemeContext } from '@ui/theme-constants'; +import { clsx } from 'clsx'; +import { useContext } from 'react'; + +import styles from './JsonNodeLabel.module.scss'; + +export const JsonNodeLabel = ({ + label, + Icon, + highlighting, +}: { + label: string; + Icon: IconComponent; + highlighting?: JsonNodeHighlighting | undefined; +}) => { + const { theme } = useContext(ThemeContext); + + return ( + + + + {label} + + ); +}; diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeValue.module.scss b/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeValue.module.scss new file mode 100644 index 0000000000..3552c2bf0d --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeValue.module.scss @@ -0,0 +1,16 @@ +.text { + align-items: center; + box-sizing: border-box; + color: var(--t-font-color-tertiary); + display: inline-flex; + height: 24px; + line-height: 1; +} + +.blue { + color: var(--t-color-blue8); +} + +.red { + color: var(--t-color-red8); +} diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeValue.module.scss.d.ts b/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeValue.module.scss.d.ts new file mode 100644 index 0000000000..da5e6e7c22 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeValue.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly text: 'text'; + readonly blue: 'blue'; + readonly red: 'red'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeValue.tsx b/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeValue.tsx new file mode 100644 index 0000000000..8c2e7f6bc8 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/components/internal/JsonNodeValue.tsx @@ -0,0 +1,32 @@ +import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow'; +import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting'; +import { clsx } from 'clsx'; + +import styles from './JsonNodeValue.module.scss'; + +export const JsonNodeValue = ({ + valueAsString, + highlighting, +}: { + valueAsString: string; + highlighting?: JsonNodeHighlighting | undefined; +}) => { + const { onNodeValueClick } = useJsonTreeContextOrThrow(); + + const handleClick = () => { + onNodeValueClick?.(valueAsString); + }; + + return ( + + {valueAsString} + + ); +}; diff --git a/packages/twenty-ui/src/json-visualizer/contexts/JsonTreeContext.tsx b/packages/twenty-ui/src/json-visualizer/contexts/JsonTreeContext.tsx new file mode 100644 index 0000000000..5048c0d320 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/contexts/JsonTreeContext.tsx @@ -0,0 +1,21 @@ +import { type GetJsonNodeHighlighting } from '@ui/json-visualizer/types/GetJsonNodeHighlighting'; +import { createContext } from 'react'; + +export type ShouldExpandNodeInitiallyProps = { keyPath: string; depth: number }; + +export type JsonTreeContextType = { + getNodeHighlighting?: GetJsonNodeHighlighting; + shouldExpandNodeInitially: ( + params: ShouldExpandNodeInitiallyProps, + ) => boolean; + emptyStringLabel: string; + emptyArrayLabel: string; + emptyObjectLabel: string; + arrowButtonCollapsedLabel: string; + arrowButtonExpandedLabel: string; + onNodeValueClick?: (valueAsString: string) => void; +}; + +export const JsonTreeContext = createContext( + undefined, +); diff --git a/packages/twenty-ui/src/json-visualizer/hooks/useJsonTreeContextOrThrow.tsx b/packages/twenty-ui/src/json-visualizer/hooks/useJsonTreeContextOrThrow.tsx new file mode 100644 index 0000000000..568fb3c07f --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/hooks/useJsonTreeContextOrThrow.tsx @@ -0,0 +1,15 @@ +import { JsonTreeContext } from '@ui/json-visualizer/contexts/JsonTreeContext'; +import { useContext } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +export const useJsonTreeContextOrThrow = () => { + const value = useContext(JsonTreeContext); + + if (!isDefined(value)) { + throw new Error( + 'useJsonTreeContextOrThrow must be used within a JsonTreeContextProvider', + ); + } + + return value; +}; diff --git a/packages/twenty-ui/src/json-visualizer/index.ts b/packages/twenty-ui/src/json-visualizer/index.ts index a3046804c0..0cc10da58e 100644 --- a/packages/twenty-ui/src/json-visualizer/index.ts +++ b/packages/twenty-ui/src/json-visualizer/index.ts @@ -7,4 +7,20 @@ * |___/ */ -export {}; +export { JsonArrayNode } from './components/JsonArrayNode'; +export { JsonNestedNode } from './components/JsonNestedNode'; +export { JsonNode } from './components/JsonNode'; +export { JsonObjectNode } from './components/JsonObjectNode'; +export { JsonTree } from './components/JsonTree'; +export { JsonTreeContextProvider } from './components/JsonTreeContextProvider'; +export { JsonValueNode } from './components/JsonValueNode'; +export type { + ShouldExpandNodeInitiallyProps, + JsonTreeContextType, +} from './contexts/JsonTreeContext'; +export { JsonTreeContext } from './contexts/JsonTreeContext'; +export { useJsonTreeContextOrThrow } from './hooks/useJsonTreeContextOrThrow'; +export type { GetJsonNodeHighlighting } from './types/GetJsonNodeHighlighting'; +export type { JsonNodeHighlighting } from './types/JsonNodeHighlighting'; +export { isArray } from './utils/isArray'; +export { isTwoFirstDepths } from './utils/isTwoFirstDepths'; diff --git a/packages/twenty-ui/src/json-visualizer/types/GetJsonNodeHighlighting.ts b/packages/twenty-ui/src/json-visualizer/types/GetJsonNodeHighlighting.ts new file mode 100644 index 0000000000..412a0800a6 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/types/GetJsonNodeHighlighting.ts @@ -0,0 +1,5 @@ +import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting'; + +export type GetJsonNodeHighlighting = ( + keyPath: string, +) => JsonNodeHighlighting | undefined; diff --git a/packages/twenty-ui/src/json-visualizer/types/JsonNodeHighlighting.ts b/packages/twenty-ui/src/json-visualizer/types/JsonNodeHighlighting.ts new file mode 100644 index 0000000000..845e106fc0 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/types/JsonNodeHighlighting.ts @@ -0,0 +1,5 @@ +import { type ThemeColor } from '@ui/theme'; + +export type JsonNodeHighlighting = + | Extract + | 'partial-blue'; diff --git a/packages/twenty-ui/src/json-visualizer/utils/isArray.ts b/packages/twenty-ui/src/json-visualizer/utils/isArray.ts new file mode 100644 index 0000000000..a3ce234cee --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/utils/isArray.ts @@ -0,0 +1,5 @@ +import { isArray as _isArray } from '@sniptt/guards'; + +export const isArray = ( + value: unknown, +): value is unknown[] | readonly unknown[] => _isArray(value); diff --git a/packages/twenty-ui/src/json-visualizer/utils/isTwoFirstDepths.ts b/packages/twenty-ui/src/json-visualizer/utils/isTwoFirstDepths.ts new file mode 100644 index 0000000000..37ecc11a87 --- /dev/null +++ b/packages/twenty-ui/src/json-visualizer/utils/isTwoFirstDepths.ts @@ -0,0 +1,4 @@ +import { type ShouldExpandNodeInitiallyProps } from '@ui/json-visualizer/contexts/JsonTreeContext'; + +export const isTwoFirstDepths = ({ depth }: ShouldExpandNodeInitiallyProps) => + depth <= 1; diff --git a/packages/twenty-ui/src/layout/animated-expandable-container/__stories__/AnimatedExpandableContainer.stories.module.scss b/packages/twenty-ui/src/layout/animated-expandable-container/__stories__/AnimatedExpandableContainer.stories.module.scss new file mode 100644 index 0000000000..37fa7b83dd --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-expandable-container/__stories__/AnimatedExpandableContainer.stories.module.scss @@ -0,0 +1,56 @@ +.button { + padding: var(--t-spacing-2) var(--t-spacing-4); + background-color: var(--t-color-blue10); + color: var(--t-font-color-primary); + border: none; + border-radius: var(--t-spacing-1); + cursor: pointer; + margin-bottom: var(--t-spacing-3); + + &:hover { + background-color: var(--t-color-blue8); + } +} + +.buttonWrapper { + height: auto; + width: auto; +} + +// dimension === 'width' +.buttonWrapperWidth { + height: 300px; +} + +// dimension === 'height' +.buttonWrapperHeight { + width: 600px; +} + +.expandableWrapper { + background-color: var(--t-background-primary); + height: 100%; + width: 100%; +} + +.content { + padding: var(--t-spacing-3); + height: auto; + width: auto; + + p { + color: var(--t-font-color-primary); + margin-bottom: var(--t-spacing-2); + font-size: var(--t-font-size-md); + } +} + +// dimension === 'height' && mode === 'scroll-height' +.contentFixedHeight { + height: 200px; +} + +// dimension === 'width' +.contentFixedWidth { + width: 400px; +} diff --git a/packages/twenty-ui/src/layout/animated-expandable-container/__stories__/AnimatedExpandableContainer.stories.module.scss.d.ts b/packages/twenty-ui/src/layout/animated-expandable-container/__stories__/AnimatedExpandableContainer.stories.module.scss.d.ts new file mode 100644 index 0000000000..3eb9f5c705 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-expandable-container/__stories__/AnimatedExpandableContainer.stories.module.scss.d.ts @@ -0,0 +1,11 @@ +declare const classNames: { + readonly button: 'button'; + readonly buttonWrapper: 'buttonWrapper'; + readonly buttonWrapperWidth: 'buttonWrapperWidth'; + readonly buttonWrapperHeight: 'buttonWrapperHeight'; + readonly expandableWrapper: 'expandableWrapper'; + readonly content: 'content'; + readonly contentFixedHeight: 'contentFixedHeight'; + readonly contentFixedWidth: 'contentFixedWidth'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/layout/animated-expandable-container/__stories__/AnimatedExpandableContainer.stories.tsx b/packages/twenty-ui/src/layout/animated-expandable-container/__stories__/AnimatedExpandableContainer.stories.tsx new file mode 100644 index 0000000000..75d16cb3a9 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-expandable-container/__stories__/AnimatedExpandableContainer.stories.tsx @@ -0,0 +1,151 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { ComponentDecorator } from '@ui/testing'; +import { clsx } from 'clsx'; +import { useState } from 'react'; +import { AnimatedExpandableContainer } from '../components/AnimatedExpandableContainer'; + +import styles from './AnimatedExpandableContainer.stories.module.scss'; + +type AnimatedExpandableContainerWithButtonProps = { + isExpanded: boolean; + dimension: 'width' | 'height'; + mode: 'scroll-height' | 'fit-content'; + animationDurations: + | { + opacity: number; + size: number; + } + | 'default'; +}; + +const AnimatedExpandableContainerWithButton = ({ + isExpanded: initialIsExpanded, + ...args +}: AnimatedExpandableContainerWithButtonProps) => { + const [isExpanded, setIsExpanded] = useState(initialIsExpanded); + + return ( +
    + + +
    +
    +

    + This is some content inside the AnimatedExpandableContainer. It + will animate smoothly when expanding or collapsing. +

    +

    + You can control the animation duration, dimension, and mode + through the Storybook controls. +

    +

    + Try different combinations to see how the container behaves with + different settings! +

    +
    +
    +
    +
    + ); +}; + +const meta: Meta = { + title: 'UI/Layout/AnimatedExpandableContainer', + component: AnimatedExpandableContainerWithButton, + decorators: [ComponentDecorator], + argTypes: { + isExpanded: { + control: 'boolean', + description: 'Controls whether the container is expanded or collapsed', + defaultValue: false, + }, + dimension: { + control: 'radio', + options: ['width', 'height'], + description: 'The dimension along which the container expands', + defaultValue: 'height', + }, + mode: { + control: 'radio', + options: ['scroll-height', 'fit-content'], + description: 'How the container should calculate its expanded size', + defaultValue: 'scroll-height', + }, + animationDurations: { + control: 'radio', + options: ['default', 'custom'], + mapping: { + default: 'default', + custom: { opacity: 0.3, size: 0.3 }, + }, + description: + 'Animation durations - either default theme values or custom values', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + isExpanded: false, + dimension: 'height', + mode: 'scroll-height', + animationDurations: 'default', + }, +}; + +export const FitContent: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + ...Default.args, + mode: 'fit-content', + }, +}; + +export const CustomDurations: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + ...Default.args, + animationDurations: { opacity: 0.8, size: 1.2 }, + }, +}; + +export const WidthAnimation: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + ...Default.args, + dimension: 'width', + }, +}; diff --git a/packages/twenty-ui/src/layout/animated-expandable-container/components/AnimatedExpandableContainer.tsx b/packages/twenty-ui/src/layout/animated-expandable-container/components/AnimatedExpandableContainer.tsx new file mode 100644 index 0000000000..afc7e0a9d6 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-expandable-container/components/AnimatedExpandableContainer.tsx @@ -0,0 +1,89 @@ +import { type AnimationDimension } from '@ui/layout/animated-expandable-container/types/AnimationDimension'; +import { type AnimationDurationObject } from '@ui/layout/animated-expandable-container/types/AnimationDurationObject'; +import { type AnimationDurations } from '@ui/layout/animated-expandable-container/types/AnimationDurations'; +import { type AnimationMode } from '@ui/layout/animated-expandable-container/types/AnimationMode'; +import { type AnimationSize } from '@ui/layout/animated-expandable-container/types/AnimationSize'; +import { getExpandableAnimationConfig } from '@ui/layout/animated-expandable-container/utils/getExpandableAnimationConfig'; +import { ThemeContext } from '@ui/theme-constants'; +import { AnimatePresence, motion } from 'framer-motion'; +import { type ReactNode, useContext, useRef, useState } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +type AnimatedExpandableContainerProps = { + children: ReactNode; + isExpanded: boolean; + dimension?: AnimationDimension; + animationDurations?: AnimationDurations; + mode?: AnimationMode; + containAnimation?: boolean; + initial?: boolean; +}; + +export const AnimatedExpandableContainer = ({ + children, + isExpanded, + dimension = 'height', + animationDurations = 'default', + mode = 'scroll-height', + containAnimation = true, + initial = true, +}: AnimatedExpandableContainerProps) => { + const { theme } = useContext(ThemeContext); + const contentRef = useRef(null); + const [size, setSize] = useState(0); + + const normalDuration = theme.animation.duration.normal; + + const actualDurations: AnimationDurationObject = + animationDurations === 'default' + ? { + opacity: normalDuration, + size: normalDuration, + } + : animationDurations; + + const updateSize = () => { + if ( + mode === 'scroll-height' && + dimension === 'height' && + isDefined(contentRef.current) + ) { + setSize(contentRef.current.scrollHeight); + } + }; + + const motionAnimationVariants = getExpandableAnimationConfig( + isExpanded, + dimension, + actualDurations.opacity, + actualDurations.size, + mode === 'fit-content' ? 'fit-content' : size, + ); + + return ( + + {isExpanded && ( + + {children} + + )} + + ); +}; diff --git a/packages/twenty-ui/src/layout/animated-expandable-container/types/AnimationDimension.ts b/packages/twenty-ui/src/layout/animated-expandable-container/types/AnimationDimension.ts new file mode 100644 index 0000000000..4cec5d0c72 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-expandable-container/types/AnimationDimension.ts @@ -0,0 +1 @@ +export type AnimationDimension = 'width' | 'height'; diff --git a/packages/twenty-ui/src/layout/animated-expandable-container/types/AnimationDurationObject.ts b/packages/twenty-ui/src/layout/animated-expandable-container/types/AnimationDurationObject.ts new file mode 100644 index 0000000000..3867129f13 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-expandable-container/types/AnimationDurationObject.ts @@ -0,0 +1,4 @@ +export type AnimationDurationObject = { + opacity: number; + size: number; +}; diff --git a/packages/twenty-ui/src/layout/animated-expandable-container/types/AnimationDurations.ts b/packages/twenty-ui/src/layout/animated-expandable-container/types/AnimationDurations.ts new file mode 100644 index 0000000000..c3006aaf10 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-expandable-container/types/AnimationDurations.ts @@ -0,0 +1,3 @@ +import { type AnimationDurationObject } from '@ui/layout/animated-expandable-container/types/AnimationDurationObject'; + +export type AnimationDurations = AnimationDurationObject | 'default'; diff --git a/packages/twenty-ui/src/layout/animated-expandable-container/types/AnimationMode.ts b/packages/twenty-ui/src/layout/animated-expandable-container/types/AnimationMode.ts new file mode 100644 index 0000000000..e7acddfee9 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-expandable-container/types/AnimationMode.ts @@ -0,0 +1 @@ +export type AnimationMode = 'scroll-height' | 'fit-content'; diff --git a/packages/twenty-ui/src/layout/animated-expandable-container/types/AnimationSize.ts b/packages/twenty-ui/src/layout/animated-expandable-container/types/AnimationSize.ts new file mode 100644 index 0000000000..c1824a0549 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-expandable-container/types/AnimationSize.ts @@ -0,0 +1 @@ +export type AnimationSize = number | 'fit-content'; diff --git a/packages/twenty-ui/src/layout/animated-expandable-container/utils/getCommonStyles.ts b/packages/twenty-ui/src/layout/animated-expandable-container/utils/getCommonStyles.ts new file mode 100644 index 0000000000..0a7e82ff16 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-expandable-container/utils/getCommonStyles.ts @@ -0,0 +1,12 @@ +import { type AnimationDimension } from '@ui/layout/animated-expandable-container/types/AnimationDimension'; +import { getTransitionValues } from '@ui/layout/animated-expandable-container/utils/getTransitionValues'; + +export const getCommonStyles = ( + dimension: AnimationDimension, + opacityDuration: number, + sizeDuration: number, +) => ({ + opacity: 0, + [dimension]: 0, + ...getTransitionValues(dimension, opacityDuration, sizeDuration), +}); diff --git a/packages/twenty-ui/src/layout/animated-expandable-container/utils/getExpandableAnimationConfig.ts b/packages/twenty-ui/src/layout/animated-expandable-container/utils/getExpandableAnimationConfig.ts new file mode 100644 index 0000000000..59e2cc5e22 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-expandable-container/utils/getExpandableAnimationConfig.ts @@ -0,0 +1,30 @@ +import { type AnimationDimension } from '@ui/layout/animated-expandable-container/types/AnimationDimension'; +import { type AnimationSize } from '@ui/layout/animated-expandable-container/types/AnimationSize'; +import { getCommonStyles } from '@ui/layout/animated-expandable-container/utils/getCommonStyles'; +import { getTransitionValues } from '@ui/layout/animated-expandable-container/utils/getTransitionValues'; + +export const getExpandableAnimationConfig = ( + isExpanded: boolean, + dimension: AnimationDimension, + opacityDuration: number, + sizeDuration: number, + size: AnimationSize, +) => ({ + initial: { + ...getCommonStyles(dimension, opacityDuration, sizeDuration), + }, + animate: { + opacity: 1, + [dimension]: isExpanded + ? size === 'fit-content' + ? 'fit-content' + : dimension === 'width' + ? '100%' + : size + : 0, + ...getTransitionValues(dimension, opacityDuration, sizeDuration), + }, + exit: { + ...getCommonStyles(dimension, opacityDuration, sizeDuration), + }, +}); diff --git a/packages/twenty-ui/src/layout/animated-expandable-container/utils/getTransitionValues.ts b/packages/twenty-ui/src/layout/animated-expandable-container/utils/getTransitionValues.ts new file mode 100644 index 0000000000..420966dbd4 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-expandable-container/utils/getTransitionValues.ts @@ -0,0 +1,18 @@ +import { type AnimationDimension } from '@ui/layout/animated-expandable-container/types/AnimationDimension'; + +export const getTransitionValues = ( + dimension: AnimationDimension, + opacityDuration: number, + sizeDuration: number, +) => ({ + transition: { + opacity: { + duration: opacityDuration, + ease: 'easeInOut', + }, + [dimension]: { + duration: sizeDuration, + ease: 'easeInOut', + }, + }, +}); diff --git a/packages/twenty-ui/src/layout/animated-placeholder/components/AnimatedPlaceholder.module.scss b/packages/twenty-ui/src/layout/animated-placeholder/components/AnimatedPlaceholder.module.scss new file mode 100644 index 0000000000..26232d302a --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-placeholder/components/AnimatedPlaceholder.module.scss @@ -0,0 +1,18 @@ +.container { + display: flex; + align-items: center; + justify-content: center; + position: relative; + min-height: 100px; +} + +.backgroundImage { + max-height: 160px; + max-width: 160px; +} + +// error404 and error500 placeholders use a larger background illustration +.backgroundImageLarge { + max-height: 245px; + max-width: 245px; +} diff --git a/packages/twenty-ui/src/layout/animated-placeholder/components/AnimatedPlaceholder.module.scss.d.ts b/packages/twenty-ui/src/layout/animated-placeholder/components/AnimatedPlaceholder.module.scss.d.ts new file mode 100644 index 0000000000..0cb921e3e9 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-placeholder/components/AnimatedPlaceholder.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly container: 'container'; + readonly backgroundImage: 'backgroundImage'; + readonly backgroundImageLarge: 'backgroundImageLarge'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/layout/animated-placeholder/components/AnimatedPlaceholder.tsx b/packages/twenty-ui/src/layout/animated-placeholder/components/AnimatedPlaceholder.tsx new file mode 100644 index 0000000000..e2b2b275ea --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-placeholder/components/AnimatedPlaceholder.tsx @@ -0,0 +1,96 @@ +import { clsx } from 'clsx'; +import { animate, motion, useMotionValue, useTransform } from 'framer-motion'; +import { useContext, useEffect } from 'react'; + +import { BACKGROUND } from '@ui/layout/animated-placeholder/constants/Background'; +import { DARK_BACKGROUND } from '@ui/layout/animated-placeholder/constants/DarkBackground'; +import { DARK_MOVING_IMAGE } from '@ui/layout/animated-placeholder/constants/DarkMovingImage'; +import { MOVING_IMAGE } from '@ui/layout/animated-placeholder/constants/MovingImage'; +import { ThemeContext } from '@ui/theme-constants'; + +import styles from './AnimatedPlaceholder.module.scss'; + +const getMovingImageSize = (type: string) => + type === 'error500' || type === 'error404' ? '185px' : '130px'; + +export type AnimatedPlaceholderType = + | keyof typeof BACKGROUND + | keyof typeof MOVING_IMAGE; + +type AnimatedPlaceholderProps = { + type: AnimatedPlaceholderType; +}; + +export const AnimatedPlaceholder = ({ type }: AnimatedPlaceholderProps) => { + const { colorScheme } = useContext(ThemeContext); + + const x = useMotionValue(window.innerWidth / 2); + const y = useMotionValue(window.innerHeight / 2); + + const translateX = useTransform(x, [0, window.innerWidth], [-2, 2]); + const translateY = useTransform(y, [0, window.innerHeight], [-2, 2]); + + useEffect(() => { + const handleMove = (event: MouseEvent | TouchEvent) => { + const clientX = + 'touches' in event ? event.touches[0].clientX : event.clientX; + const clientY = + 'touches' in event ? event.touches[0].clientY : event.clientY; + + x.set(clientX); + y.set(clientY); + }; + + const handleLeave = () => { + animate(x, window.innerWidth / 2, { + type: 'spring', + stiffness: 100, + damping: 10, + }); + animate(y, window.innerHeight / 2, { + type: 'spring', + stiffness: 100, + damping: 10, + }); + }; + + window.addEventListener('mousemove', handleMove); + window.addEventListener('touchmove', handleMove); + window.document.addEventListener('mouseleave', handleLeave); + + return () => { + window.removeEventListener('mousemove', handleMove); + window.removeEventListener('touchmove', handleMove); + window.document.removeEventListener('mouseleave', handleLeave); + }; + }, [x, y]); + + return ( +
    + + +
    + ); +}; diff --git a/packages/twenty-ui/src/layout/animated-placeholder/components/EmptyPlaceholderStyled.module.scss b/packages/twenty-ui/src/layout/animated-placeholder/components/EmptyPlaceholderStyled.module.scss new file mode 100644 index 0000000000..1fd5d982c8 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-placeholder/components/EmptyPlaceholderStyled.module.scss @@ -0,0 +1,36 @@ +.emptyContainer { + align-items: center; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + gap: var(--t-spacing-6); + justify-content: center; + text-align: center; +} + +.emptyTextContainer { + align-items: center; + display: flex; + flex-direction: column; + gap: var(--t-spacing-2); + justify-content: center; + text-align: center; + width: 100%; +} + +.emptyTitle { + color: var(--t-font-color-primary); + font-size: var(--t-font-size-lg); + font-weight: var(--t-font-weight-semi-bold); +} + +.emptySubTitle { + color: var(--t-font-color-tertiary); + font-size: var(--t-font-size-sm); + font-weight: var(--t-font-weight-regular); + line-height: var(--t-text-line-height-lg); + max-height: 2.8em; + overflow: hidden; + width: 50%; +} diff --git a/packages/twenty-ui/src/layout/animated-placeholder/components/EmptyPlaceholderStyled.module.scss.d.ts b/packages/twenty-ui/src/layout/animated-placeholder/components/EmptyPlaceholderStyled.module.scss.d.ts new file mode 100644 index 0000000000..2dab28b357 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-placeholder/components/EmptyPlaceholderStyled.module.scss.d.ts @@ -0,0 +1,7 @@ +declare const classNames: { + readonly emptyContainer: 'emptyContainer'; + readonly emptyTextContainer: 'emptyTextContainer'; + readonly emptyTitle: 'emptyTitle'; + readonly emptySubTitle: 'emptySubTitle'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx b/packages/twenty-ui/src/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx new file mode 100644 index 0000000000..2d5dba330e --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx @@ -0,0 +1,77 @@ +import { clsx } from 'clsx'; +import { type HTMLMotionProps, motion } from 'framer-motion'; +import { type ComponentPropsWithoutRef, type ReactNode } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +import styles from './EmptyPlaceholderStyled.module.scss'; + +type AnimatedPlaceholderEmptyContainerProps = Pick< + HTMLMotionProps<'div'>, + 'initial' | 'animate' | 'transition' +> & { + children?: ReactNode; + className?: string; + width?: number; +}; + +export const AnimatedPlaceholderEmptyContainer = ({ + children, + className, + width, + initial, + animate, + transition, +}: AnimatedPlaceholderEmptyContainerProps) => { + return ( + + {children} + + ); +}; + +export const EMPTY_PLACEHOLDER_TRANSITION_PROPS = { + initial: { opacity: 0 }, + animate: { opacity: 1 }, + transition: { + duration: 0.15, + }, +}; + +export const AnimatedPlaceholderEmptyTextContainer = ({ + className, + ...rest +}: ComponentPropsWithoutRef<'div'>) => ( +
    +); + +export const AnimatedPlaceholderEmptyTitle = ({ + className, + ...rest +}: ComponentPropsWithoutRef<'div'>) => ( +
    +); + +export const AnimatedPlaceholderEmptySubTitle = ({ + className, + ...rest +}: ComponentPropsWithoutRef<'div'>) => ( +
    +); diff --git a/packages/twenty-ui/src/layout/animated-placeholder/components/ErrorPlaceholderStyled.module.scss b/packages/twenty-ui/src/layout/animated-placeholder/components/ErrorPlaceholderStyled.module.scss new file mode 100644 index 0000000000..dc44510758 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-placeholder/components/ErrorPlaceholderStyled.module.scss @@ -0,0 +1,36 @@ +.errorContainer { + align-items: center; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + gap: var(--t-spacing-8); + justify-content: center; + text-align: center; +} + +.errorTextContainer { + align-items: center; + display: flex; + flex-direction: column; + gap: var(--t-spacing-4); + justify-content: center; + text-align: center; + width: 100%; +} + +.errorTitle { + color: var(--t-font-color-primary); + font-size: var(--t-font-size-xl); + font-weight: var(--t-font-weight-semi-bold); + line-height: var(--t-text-line-height-lg); +} + +.errorSubTitle { + color: var(--t-font-color-tertiary); + font-size: var(--t-font-size-xs); + font-weight: var(--t-font-weight-regular); + line-height: var(--t-text-line-height-md); + max-height: 2.4em; + overflow: hidden; +} diff --git a/packages/twenty-ui/src/layout/animated-placeholder/components/ErrorPlaceholderStyled.module.scss.d.ts b/packages/twenty-ui/src/layout/animated-placeholder/components/ErrorPlaceholderStyled.module.scss.d.ts new file mode 100644 index 0000000000..b8784af7aa --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-placeholder/components/ErrorPlaceholderStyled.module.scss.d.ts @@ -0,0 +1,7 @@ +declare const classNames: { + readonly errorContainer: 'errorContainer'; + readonly errorTextContainer: 'errorTextContainer'; + readonly errorTitle: 'errorTitle'; + readonly errorSubTitle: 'errorSubTitle'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/layout/animated-placeholder/components/ErrorPlaceholderStyled.tsx b/packages/twenty-ui/src/layout/animated-placeholder/components/ErrorPlaceholderStyled.tsx new file mode 100644 index 0000000000..a7b6e561ca --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-placeholder/components/ErrorPlaceholderStyled.tsx @@ -0,0 +1,48 @@ +import { clsx } from 'clsx'; +import { type ComponentPropsWithoutRef } from 'react'; + +import styles from './ErrorPlaceholderStyled.module.scss'; + +export const AnimatedPlaceholderErrorContainer = ({ + className, + ...rest +}: ComponentPropsWithoutRef<'div'>) => ( +
    +); + +export const AnimatedPlaceholderErrorTextContainer = ({ + className, + ...rest +}: ComponentPropsWithoutRef<'div'>) => ( +
    +); + +export const AnimatedPlaceholderErrorTitle = ({ + className, + ...rest +}: ComponentPropsWithoutRef<'div'>) => ( +
    +); + +export const AnimatedPlaceholderErrorSubTitle = ({ + className, + ...rest +}: ComponentPropsWithoutRef<'div'>) => ( +
    +); diff --git a/packages/twenty-ui/src/layout/animated-placeholder/constants/Background.ts b/packages/twenty-ui/src/layout/animated-placeholder/constants/Background.ts new file mode 100644 index 0000000000..d485b8f967 --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-placeholder/constants/Background.ts @@ -0,0 +1,18 @@ +export const BACKGROUND: Record = { + noFile: '/images/placeholders/background/no_file_bg.png', + noNote: '/images/placeholders/background/no_note_bg.png', + noRecord: '/images/placeholders/background/no_record_bg.png', + noMatchRecord: '/images/placeholders/background/no_match_record_bg.png', + noTask: '/images/placeholders/background/no_task_bg.png', + errorIndex: '/images/placeholders/background/error_index_bg.png', + emptyTimeline: '/images/placeholders/background/empty_timeline_bg.png', + loadingMessages: '/images/placeholders/background/loading_messages_bg.png', + loadingAccounts: '/images/placeholders/background/loading_accounts_bg.png', + emptyFunctions: '/images/placeholders/background/empty_functions_bg.png', + emptyInbox: '/images/placeholders/background/empty_inbox_bg.png', + error404: '/images/placeholders/background/404_bg.png', + error500: '/images/placeholders/background/500_bg.png', + noDeletedRecord: '/images/placeholders/background/no_deleted_record_bg.png', + noWidgets: '/images/placeholders/background/no_widgets_bg.png', + notShared: '/images/placeholders/background/not_shared_bg.png', +}; diff --git a/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkBackground.ts b/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkBackground.ts new file mode 100644 index 0000000000..3371f4990a --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkBackground.ts @@ -0,0 +1,19 @@ +export const DARK_BACKGROUND: Record = { + noFile: '/images/placeholders/dark-background/no_file_bg.png', + noNote: '/images/placeholders/dark-background/no_note_bg.png', + noRecord: '/images/placeholders/dark-background/no_record_bg.png', + noMatchRecord: '/images/placeholders/dark-background/no_match_record_bg.png', + noTask: '/images/placeholders/dark-background/no_task_bg.png', + errorIndex: '/images/placeholders/dark-background/error_index_bg.png', + emptyTimeline: '/images/placeholders/dark-background/empty_timeline_bg.png', + emptyInbox: '/images/placeholders/dark-background/empty_inbox_bg.png', + error404: '/images/placeholders/dark-background/404_bg.png', + error500: '/images/placeholders/dark-background/500_bg.png', + loadingMessages: '/images/placeholders/background/loading_messages_bg.png', + loadingAccounts: '/images/placeholders/background/loading_accounts_bg.png', + emptyFunctions: '/images/placeholders/dark-background/empty_functions_bg.png', + noDeletedRecord: + '/images/placeholders/dark-background/no_deleted_record_bg.png', + noWidgets: '/images/placeholders/dark-background/no_widgets_bg.png', + notShared: '/images/placeholders/dark-background/not_shared_bg.png', +}; diff --git a/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkMovingImage.ts b/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkMovingImage.ts new file mode 100644 index 0000000000..63633ada6f --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkMovingImage.ts @@ -0,0 +1,19 @@ +export const DARK_MOVING_IMAGE: Record = { + noFile: '/images/placeholders/dark-moving-image/no_file.png', + noNote: '/images/placeholders/dark-moving-image/no_note.png', + noRecord: '/images/placeholders/dark-moving-image/no_record.png', + noMatchRecord: '/images/placeholders/dark-moving-image/no_match_record.png', + noTask: '/images/placeholders/dark-moving-image/no_task.png', + errorIndex: '/images/placeholders/dark-moving-image/error_index.png', + emptyTimeline: '/images/placeholders/dark-moving-image/empty_timeline.png', + emptyInbox: '/images/placeholders/dark-moving-image/empty_inbox.png', + error404: '/images/placeholders/dark-moving-image/404.png', + error500: '/images/placeholders/dark-moving-image/500.png', + loadingMessages: '/images/placeholders/moving-image/loading_messages.png', + loadingAccounts: '/images/placeholders/moving-image/loading_accounts.png', + emptyFunctions: '/images/placeholders/dark-moving-image/empty_functions.png', + noDeletedRecord: + '/images/placeholders/dark-moving-image/no_deleted_record.png', + noWidgets: '/images/placeholders/dark-moving-image/no_widgets.png', + notShared: '/images/placeholders/dark-moving-image/not_shared.png', +}; diff --git a/packages/twenty-ui/src/layout/animated-placeholder/constants/MovingImage.ts b/packages/twenty-ui/src/layout/animated-placeholder/constants/MovingImage.ts new file mode 100644 index 0000000000..afedead50f --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-placeholder/constants/MovingImage.ts @@ -0,0 +1,18 @@ +export const MOVING_IMAGE: Record = { + noFile: '/images/placeholders/moving-image/no_file.png', + noNote: '/images/placeholders/moving-image/no_note.png', + noRecord: '/images/placeholders/moving-image/no_record.png', + noMatchRecord: '/images/placeholders/moving-image/no_match_record.png', + noTask: '/images/placeholders/moving-image/no_task.png', + errorIndex: '/images/placeholders/moving-image/error_index.png', + emptyTimeline: '/images/placeholders/moving-image/empty_timeline.png', + loadingMessages: '/images/placeholders/moving-image/loading_messages.png', + loadingAccounts: '/images/placeholders/moving-image/loading_accounts.png', + emptyFunctions: '/images/placeholders/moving-image/empty_functions.png', + emptyInbox: '/images/placeholders/moving-image/empty_inbox.png', + error404: '/images/placeholders/moving-image/404.png', + error500: '/images/placeholders/moving-image/500.png', + noDeletedRecord: '/images/placeholders/moving-image/no_deleted_record.png', + noWidgets: '/images/placeholders/moving-image/no_widgets.png', + notShared: '/images/placeholders/moving-image/not_shared.png', +}; diff --git a/packages/twenty-ui/src/layout/animated-placeholder/index.ts b/packages/twenty-ui/src/layout/animated-placeholder/index.ts new file mode 100644 index 0000000000..74da7bd40c --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-placeholder/index.ts @@ -0,0 +1,7 @@ +export * from './components/AnimatedPlaceholder'; +export * from './components/EmptyPlaceholderStyled'; +export * from './components/ErrorPlaceholderStyled'; +export * from './constants/Background'; +export * from './constants/DarkBackground'; +export * from './constants/DarkMovingImage'; +export * from './constants/MovingImage'; diff --git a/packages/twenty-ui/src/layout/card/components/Card.module.scss b/packages/twenty-ui/src/layout/card/components/Card.module.scss new file mode 100644 index 0000000000..804772e08a --- /dev/null +++ b/packages/twenty-ui/src/layout/card/components/Card.module.scss @@ -0,0 +1,16 @@ +.card { + background-color: var(--card-background-color, transparent); + border: 1px solid var(--t-border-color-medium); + border-radius: var(--t-border-radius-sm); + color: var(--t-font-color-secondary); + overflow: hidden; + width: auto; + + &[data-rounded] { + border-radius: var(--t-border-radius-md); + } + + &[data-full-width] { + width: 100%; + } +} diff --git a/packages/twenty-ui/src/layout/card/components/Card.module.scss.d.ts b/packages/twenty-ui/src/layout/card/components/Card.module.scss.d.ts new file mode 100644 index 0000000000..4e80ea28d1 --- /dev/null +++ b/packages/twenty-ui/src/layout/card/components/Card.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly card: 'card'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/layout/card/components/Card.tsx b/packages/twenty-ui/src/layout/card/components/Card.tsx new file mode 100644 index 0000000000..151324a167 --- /dev/null +++ b/packages/twenty-ui/src/layout/card/components/Card.tsx @@ -0,0 +1,41 @@ +import { clsx } from 'clsx'; +import { type ComponentPropsWithoutRef, type CSSProperties } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +import styles from './Card.module.scss'; + +type CardProps = ComponentPropsWithoutRef<'div'> & { + fullWidth?: boolean; + rounded?: boolean; + backgroundColor?: string; +}; + +export const Card = ({ + children, + className, + fullWidth, + rounded, + backgroundColor, + style, + ...rest +}: CardProps) => { + return ( +
    + {children} +
    + ); +}; diff --git a/packages/twenty-ui/src/layout/card/components/CardContent.module.scss b/packages/twenty-ui/src/layout/card/components/CardContent.module.scss new file mode 100644 index 0000000000..350564f33f --- /dev/null +++ b/packages/twenty-ui/src/layout/card/components/CardContent.module.scss @@ -0,0 +1,24 @@ +.cardContent { + background-color: var(--t-background-secondary); + padding: var(--t-spacing-4); + + border-bottom: none; + + cursor: default; + + &[data-divider] { + border-bottom: 1px solid var(--t-border-color-medium); + } + + &[data-clickable] { + cursor: pointer; + } + + &:hover { + background: transparent; + } + + &[data-hover-highlight]:hover { + background: var(--t-background-transparent-light); + } +} diff --git a/packages/twenty-ui/src/layout/card/components/CardContent.module.scss.d.ts b/packages/twenty-ui/src/layout/card/components/CardContent.module.scss.d.ts new file mode 100644 index 0000000000..d4f1beb46b --- /dev/null +++ b/packages/twenty-ui/src/layout/card/components/CardContent.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly cardContent: 'cardContent'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/layout/card/components/CardContent.tsx b/packages/twenty-ui/src/layout/card/components/CardContent.tsx new file mode 100644 index 0000000000..7227520f82 --- /dev/null +++ b/packages/twenty-ui/src/layout/card/components/CardContent.tsx @@ -0,0 +1,35 @@ +import { clsx } from 'clsx'; +import { type HTMLMotionProps, motion } from 'framer-motion'; +import { type ReactNode } from 'react'; + +import styles from './CardContent.module.scss'; + +type CardContentProps = { + children?: ReactNode; + className?: string; + divider?: boolean; + isClickable?: boolean; + hasHoverHighlight?: boolean; +} & Omit, 'theme'>; + +export const CardContent = ({ + children, + className, + divider, + isClickable, + hasHoverHighlight, + ...rest +}: CardContentProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/twenty-ui/src/layout/card/components/CardFooter.module.scss b/packages/twenty-ui/src/layout/card/components/CardFooter.module.scss new file mode 100644 index 0000000000..6f62704e3d --- /dev/null +++ b/packages/twenty-ui/src/layout/card/components/CardFooter.module.scss @@ -0,0 +1,11 @@ +.cardFooter { + background-color: var(--t-background-primary); + border-top: 1px solid var(--t-border-color-medium); + font-size: var(--t-font-size-sm); + padding: var(--t-spacing-2) var(--t-spacing-4); + + // divider defaults to true; only an explicit false removes the border + &[data-no-divider] { + border-top: 0; + } +} diff --git a/packages/twenty-ui/src/layout/card/components/CardFooter.module.scss.d.ts b/packages/twenty-ui/src/layout/card/components/CardFooter.module.scss.d.ts new file mode 100644 index 0000000000..d56ecabeec --- /dev/null +++ b/packages/twenty-ui/src/layout/card/components/CardFooter.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly cardFooter: 'cardFooter'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/layout/card/components/CardFooter.tsx b/packages/twenty-ui/src/layout/card/components/CardFooter.tsx new file mode 100644 index 0000000000..93fdc1ba84 --- /dev/null +++ b/packages/twenty-ui/src/layout/card/components/CardFooter.tsx @@ -0,0 +1,26 @@ +import { clsx } from 'clsx'; +import { type ComponentPropsWithoutRef } from 'react'; + +import styles from './CardFooter.module.scss'; + +type CardFooterProps = ComponentPropsWithoutRef<'div'> & { + divider?: boolean; +}; + +export const CardFooter = ({ + children, + className, + divider, + ...rest +}: CardFooterProps) => { + return ( +
    + {children} +
    + ); +}; diff --git a/packages/twenty-ui/src/layout/card/components/CardHeader.module.scss b/packages/twenty-ui/src/layout/card/components/CardHeader.module.scss new file mode 100644 index 0000000000..9dc125916e --- /dev/null +++ b/packages/twenty-ui/src/layout/card/components/CardHeader.module.scss @@ -0,0 +1,7 @@ +.cardHeader { + background-color: var(--t-background-primary); + border-bottom: 1px solid var(--t-border-color-medium); + font-size: var(--t-font-size-sm); + font-weight: var(--t-font-weight-medium); + padding: var(--t-spacing-2) var(--t-spacing-4); +} diff --git a/packages/twenty-ui/src/layout/card/components/CardHeader.module.scss.d.ts b/packages/twenty-ui/src/layout/card/components/CardHeader.module.scss.d.ts new file mode 100644 index 0000000000..0ac3454d2f --- /dev/null +++ b/packages/twenty-ui/src/layout/card/components/CardHeader.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly cardHeader: 'cardHeader'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/layout/card/components/CardHeader.tsx b/packages/twenty-ui/src/layout/card/components/CardHeader.tsx new file mode 100644 index 0000000000..1a15cd03d2 --- /dev/null +++ b/packages/twenty-ui/src/layout/card/components/CardHeader.tsx @@ -0,0 +1,19 @@ +import { clsx } from 'clsx'; +import { type ComponentPropsWithoutRef } from 'react'; + +import styles from './CardHeader.module.scss'; + +type CardHeaderProps = ComponentPropsWithoutRef<'div'>; + +export const CardHeader = ({ + children, + className, + ...rest +}: CardHeaderProps) => { + return ( + // oxlint-disable-next-line react/jsx-props-no-spreading +
    + {children} +
    + ); +}; diff --git a/packages/twenty-ui/src/layout/card/components/__stories__/Card.stories.tsx b/packages/twenty-ui/src/layout/card/components/__stories__/Card.stories.tsx new file mode 100644 index 0000000000..ecac8b70d2 --- /dev/null +++ b/packages/twenty-ui/src/layout/card/components/__stories__/Card.stories.tsx @@ -0,0 +1,31 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { ComponentDecorator } from '@ui/testing'; + +import { Card } from '../Card'; +import { CardContent } from '../CardContent'; +import { CardFooter } from '../CardFooter'; +import { CardHeader } from '../CardHeader'; + +const meta: Meta = { + title: 'UI/Layout/Card/Card', + component: Card, + decorators: [ComponentDecorator], + render: (args) => ( + // oxlint-disable-next-line react/jsx-props-no-spreading + + Lorem ipsum + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec id massa + vel odio ullamcorper molestie eu nec ipsum. Sed semper convallis + consectetur. + + Lorem ipsum + + ), + argTypes: {}, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/packages/twenty-ui/src/layout/index.ts b/packages/twenty-ui/src/layout/index.ts index a3046804c0..eb65aac5cf 100644 --- a/packages/twenty-ui/src/layout/index.ts +++ b/packages/twenty-ui/src/layout/index.ts @@ -7,4 +7,54 @@ * |___/ */ -export {}; +export { AnimatedExpandableContainer } from './animated-expandable-container/components/AnimatedExpandableContainer'; +export type { AnimationDimension } from './animated-expandable-container/types/AnimationDimension'; +export type { AnimationDurationObject } from './animated-expandable-container/types/AnimationDurationObject'; +export type { AnimationDurations } from './animated-expandable-container/types/AnimationDurations'; +export type { AnimationMode } from './animated-expandable-container/types/AnimationMode'; +export type { AnimationSize } from './animated-expandable-container/types/AnimationSize'; +export { getCommonStyles } from './animated-expandable-container/utils/getCommonStyles'; +export { getExpandableAnimationConfig } from './animated-expandable-container/utils/getExpandableAnimationConfig'; +export { getTransitionValues } from './animated-expandable-container/utils/getTransitionValues'; +export type { AnimatedPlaceholderType } from './animated-placeholder/components/AnimatedPlaceholder'; +export { AnimatedPlaceholder } from './animated-placeholder/components/AnimatedPlaceholder'; +export { + AnimatedPlaceholderEmptyContainer, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, + AnimatedPlaceholderEmptyTextContainer, + AnimatedPlaceholderEmptyTitle, + AnimatedPlaceholderEmptySubTitle, +} from './animated-placeholder/components/EmptyPlaceholderStyled'; +export { + AnimatedPlaceholderErrorContainer, + AnimatedPlaceholderErrorTextContainer, + AnimatedPlaceholderErrorTitle, + AnimatedPlaceholderErrorSubTitle, +} from './animated-placeholder/components/ErrorPlaceholderStyled'; +export { BACKGROUND } from './animated-placeholder/constants/Background'; +export { DARK_BACKGROUND } from './animated-placeholder/constants/DarkBackground'; +export { DARK_MOVING_IMAGE } from './animated-placeholder/constants/DarkMovingImage'; +export { MOVING_IMAGE } from './animated-placeholder/constants/MovingImage'; +export { Card } from './card/components/Card'; +export { CardContent } from './card/components/CardContent'; +export { CardFooter } from './card/components/CardFooter'; +export { CardHeader } from './card/components/CardHeader'; +export { Modal } from './modal/components/Modal'; +export { ModalBackdrop } from './modal/components/ModalBackdrop'; +export type { ModalContentProps } from './modal/components/ModalContent'; +export { ModalContent } from './modal/components/ModalContent'; +export type { ModalFooterProps } from './modal/components/ModalFooter'; +export { ModalFooter } from './modal/components/ModalFooter'; +export type { ModalHeaderProps } from './modal/components/ModalHeader'; +export { ModalHeader } from './modal/components/ModalHeader'; +export type { ModalOverlay } from './modal/types/ModalOverlay'; +export type { ModalPadding } from './modal/types/ModalPadding'; +export type { ModalProps } from './modal/types/ModalProps'; +export type { ModalSize } from './modal/types/ModalSize'; +export { ResizeHandle } from './resize-handle/components/ResizeHandle'; +export { useResizeHandle } from './resize-handle/hooks/useResizeHandle'; +export { + SectionAlignment, + SectionFontColor, + Section, +} from './section/components/Section'; diff --git a/packages/twenty-ui/src/layout/modal/components/Modal.module.scss b/packages/twenty-ui/src/layout/modal/components/Modal.module.scss new file mode 100644 index 0000000000..e3c64d243d --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/Modal.module.scss @@ -0,0 +1,98 @@ +.modal { + display: flex; + flex-direction: column; + box-shadow: var(--t-box-shadow-strong); + background: var(--t-background-primary); + color: var(--t-font-color-primary); + border-radius: var(--t-border-radius-md); + overflow-x: hidden; + overflow-y: auto; + z-index: var(--modal-z-index, 40); + width: auto; + height: auto; + max-height: 90dvh; + // framer-motion faded the deprecated modal in and out over the theme's + // normal duration; Base UI drives the same fade through its + // data-starting-style and data-ending-style transition states + transition: opacity duration(normal) ease-out; + + &[data-starting-style], + &[data-ending-style] { + opacity: 0; + } +} + +// The source order of the classes below replicates the precedence of the +// deprecated Linaria conditionals: fullscreen > narrowWidth and autoHeight > +// isMobile > overlay and smallBorderRadius > size + +.small { + width: var(--t-modal-size-sm-width); +} + +.medium { + width: var(--t-modal-size-md-width); +} + +.large { + width: var(--t-modal-size-lg-width); +} + +.extraLarge { + width: var(--t-modal-size-xl-width); + height: var(--t-modal-size-xl-height); +} + +.paddingNone { + padding: var(--t-spacing-0); +} + +.paddingSmall { + padding: var(--t-spacing-2); +} + +.paddingMedium { + padding: var(--t-spacing-4); +} + +.paddingLarge { + padding: var(--t-spacing-6); +} + +.overlayDark { + box-shadow: var(--t-box-shadow-super-heavy); +} + +.smallBorderRadius { + border-radius: var(--t-spacing-1); +} + +.overlayTransparent { + box-shadow: none; + background: transparent; + border-radius: 0; +} + +.mobile { + border-radius: 0; + width: var(--t-modal-size-fullscreen-width); + height: var(--t-modal-size-fullscreen-height); + max-height: none; +} + +.narrowWidth { + width: calc(400px - var(--t-spacing-32)); +} + +.autoHeight { + height: auto; +} + +.fullscreen { + box-shadow: none; + background: var(--t-background-primary); + border-radius: 0; + width: var(--t-modal-size-fullscreen-width); + height: var(--t-modal-size-fullscreen-height); + max-height: none; +} diff --git a/packages/twenty-ui/src/layout/modal/components/Modal.module.scss.d.ts b/packages/twenty-ui/src/layout/modal/components/Modal.module.scss.d.ts new file mode 100644 index 0000000000..670f48ddf0 --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/Modal.module.scss.d.ts @@ -0,0 +1,19 @@ +declare const classNames: { + readonly modal: 'modal'; + readonly small: 'small'; + readonly medium: 'medium'; + readonly large: 'large'; + readonly extraLarge: 'extraLarge'; + readonly paddingNone: 'paddingNone'; + readonly paddingSmall: 'paddingSmall'; + readonly paddingMedium: 'paddingMedium'; + readonly paddingLarge: 'paddingLarge'; + readonly overlayDark: 'overlayDark'; + readonly smallBorderRadius: 'smallBorderRadius'; + readonly overlayTransparent: 'overlayTransparent'; + readonly mobile: 'mobile'; + readonly narrowWidth: 'narrowWidth'; + readonly autoHeight: 'autoHeight'; + readonly fullscreen: 'fullscreen'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/layout/modal/components/Modal.tsx b/packages/twenty-ui/src/layout/modal/components/Modal.tsx new file mode 100644 index 0000000000..770ef69254 --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/Modal.tsx @@ -0,0 +1,122 @@ +import { Dialog } from '@base-ui/react/dialog'; +import { clsx } from 'clsx'; +import { useRef } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +import { type ModalOverlay } from '../types/ModalOverlay'; +import { type ModalPadding } from '../types/ModalPadding'; +import { type ModalProps } from '../types/ModalProps'; + +import styles from './Modal.module.scss'; +import { ModalBackdrop } from './ModalBackdrop'; + +const DEFAULT_MODAL_Z_INDEX = 40; +const DEFAULT_BACKDROP_Z_INDEX = 39; + +// Base UI's Dialog.Popup stops the propagation of these keydown events, but +// the deprecated Modal let them bubble up to global hotkey listeners. +const KEYDOWN_EVENTS_TO_PROPAGATE = new Set([ + 'ArrowUp', + 'ArrowDown', + 'ArrowLeft', + 'ArrowRight', + 'Home', + 'End', +]); + +const PADDING_CLASS_NAMES: Record = { + none: styles.paddingNone, + small: styles.paddingSmall, + medium: styles.paddingMedium, + large: styles.paddingLarge, +}; + +// The 'light' overlay is the base style of the modal, so it needs no class +const OVERLAY_CLASS_NAMES: Record = { + dark: styles.overlayDark, + light: undefined, + transparent: styles.overlayTransparent, +}; + +export const Modal = ({ + isOpen, + children, + size = 'medium', + padding = 'medium', + overlay = 'dark', + isMobile = false, + isInContainer = false, + container, + gap, + smallBorderRadius, + narrowWidth, + autoHeight, + modalZIndex = DEFAULT_MODAL_Z_INDEX, + backdropZIndex = DEFAULT_BACKDROP_Z_INDEX, + backdropTestId = 'modal-backdrop', + backdropClickOutsideId, + preventClickOutside, + onBackdropMouseDown, + modalRef: externalRef, +}: ModalProps) => { + const internalRef = useRef(null); + const resolvedRef = externalRef ?? internalRef; + + const handleBackdropMouseDown = (e: React.MouseEvent) => { + e.stopPropagation(); + onBackdropMouseDown?.(e); + }; + + // modal={false} and disablePointerDismissal keep Base UI from trapping + // focus, locking scroll or closing on outside press: like the deprecated + // Modal, open/close is fully controlled by the isOpen prop + return ( + + + + { + if (KEYDOWN_EVENTS_TO_PROPAGATE.has(event.key)) { + event.preventBaseUIHandler(); + } + }} + render={ +
    + } + > + {children} + + + + + ); +}; diff --git a/packages/twenty-ui/src/layout/modal/components/ModalBackdrop.module.scss b/packages/twenty-ui/src/layout/modal/components/ModalBackdrop.module.scss new file mode 100644 index 0000000000..3f990679c2 --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/ModalBackdrop.module.scss @@ -0,0 +1,23 @@ +.backdrop { + align-items: center; + background: var(--t-background-overlay-primary); + display: flex; + height: 100%; + justify-content: center; + left: 0; + pointer-events: auto; + position: fixed; + top: 0; + width: 100%; + z-index: var(--modal-backdrop-z-index, 39); + user-select: none; +} + +.overlayLight { + background: var(--t-background-overlay-tertiary); +} + +.inContainer { + background: var(--t-background-overlay-tertiary); + position: absolute; +} diff --git a/packages/twenty-ui/src/layout/modal/components/ModalBackdrop.module.scss.d.ts b/packages/twenty-ui/src/layout/modal/components/ModalBackdrop.module.scss.d.ts new file mode 100644 index 0000000000..f3c500808b --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/ModalBackdrop.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly backdrop: 'backdrop'; + readonly overlayLight: 'overlayLight'; + readonly inContainer: 'inContainer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/layout/modal/components/ModalBackdrop.tsx b/packages/twenty-ui/src/layout/modal/components/ModalBackdrop.tsx new file mode 100644 index 0000000000..272823db78 --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/ModalBackdrop.tsx @@ -0,0 +1,44 @@ +import { clsx } from 'clsx'; +import { forwardRef } from 'react'; + +import { type ModalOverlay } from '../types/ModalOverlay'; + +import styles from './ModalBackdrop.module.scss'; + +// The deprecated Linaria styled.div forwarded refs and accepted all native +// div props (including the data-* attributes Modal relies on), so the port +// preserves that contract. +type ModalBackdropProps = React.ComponentPropsWithoutRef<'div'> & { + overlay: ModalOverlay; + backdropZIndex: number; + isInContainer?: boolean; + 'data-testid'?: string; + 'data-click-outside-id'?: string; +}; + +export const ModalBackdrop = forwardRef( + ( + { overlay, backdropZIndex, isInContainer, className, style, ...divProps }, + ref, + ) => ( +
    + ), +); + +ModalBackdrop.displayName = 'ModalBackdrop'; diff --git a/packages/twenty-ui/src/layout/modal/components/ModalContent.module.scss b/packages/twenty-ui/src/layout/modal/components/ModalContent.module.scss new file mode 100644 index 0000000000..845c8fb165 --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/ModalContent.module.scss @@ -0,0 +1,30 @@ +.content { + align-items: stretch; + display: flex; + flex: 1 1 0%; + flex-direction: column; + justify-content: flex-start; + overflow: visible; + padding: var(--t-spacing-10); +} + +.verticallyCentered { + align-items: center; +} + +.horizontallyCentered { + justify-content: center; +} + +.overflowHidden { + overflow: hidden; +} + +.withContentPadding { + padding: var(--modal-content-padding); +} + +// noPadding wins over contentPadding, like the deprecated Linaria switch +.noPadding { + padding: 0; +} diff --git a/packages/twenty-ui/src/layout/modal/components/ModalContent.module.scss.d.ts b/packages/twenty-ui/src/layout/modal/components/ModalContent.module.scss.d.ts new file mode 100644 index 0000000000..31a89d494e --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/ModalContent.module.scss.d.ts @@ -0,0 +1,9 @@ +declare const classNames: { + readonly content: 'content'; + readonly verticallyCentered: 'verticallyCentered'; + readonly horizontallyCentered: 'horizontallyCentered'; + readonly overflowHidden: 'overflowHidden'; + readonly withContentPadding: 'withContentPadding'; + readonly noPadding: 'noPadding'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/layout/modal/components/ModalContent.tsx b/packages/twenty-ui/src/layout/modal/components/ModalContent.tsx new file mode 100644 index 0000000000..142a964753 --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/ModalContent.tsx @@ -0,0 +1,44 @@ +import { clsx } from 'clsx'; + +import styles from './ModalContent.module.scss'; + +export type ModalContentProps = React.PropsWithChildren & { + isVerticallyCentered?: boolean; + isHorizontallyCentered?: boolean; + noPadding?: boolean; + overflowHidden?: boolean; + gap?: number; + contentPadding?: number; +}; + +export const ModalContent = ({ + children, + isVerticallyCentered, + isHorizontallyCentered, + noPadding, + overflowHidden, + gap, + contentPadding, +}: ModalContentProps) => ( +
    + {children} +
    +); diff --git a/packages/twenty-ui/src/layout/modal/components/ModalFooter.module.scss b/packages/twenty-ui/src/layout/modal/components/ModalFooter.module.scss new file mode 100644 index 0000000000..6792fb2f78 --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/ModalFooter.module.scss @@ -0,0 +1,22 @@ +.footer { + align-items: center; + display: flex; + flex-direction: row; + gap: var(--t-spacing-2); + height: 60px; + justify-content: flex-end; + overflow: hidden; + padding: var(--t-spacing-5); +} + +.autoHeight { + height: auto; +} + +.centered { + justify-content: center; +} + +.smallPadding { + padding: var(--t-spacing-3); +} diff --git a/packages/twenty-ui/src/layout/modal/components/ModalFooter.module.scss.d.ts b/packages/twenty-ui/src/layout/modal/components/ModalFooter.module.scss.d.ts new file mode 100644 index 0000000000..8bd9b6a5a0 --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/ModalFooter.module.scss.d.ts @@ -0,0 +1,7 @@ +declare const classNames: { + readonly footer: 'footer'; + readonly autoHeight: 'autoHeight'; + readonly centered: 'centered'; + readonly smallPadding: 'smallPadding'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/layout/modal/components/ModalFooter.tsx b/packages/twenty-ui/src/layout/modal/components/ModalFooter.tsx new file mode 100644 index 0000000000..3df4a7fa15 --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/ModalFooter.tsx @@ -0,0 +1,30 @@ +import { clsx } from 'clsx'; + +import styles from './ModalFooter.module.scss'; + +export type ModalFooterProps = React.PropsWithChildren & { + autoHeight?: boolean; + centered?: boolean; + smallPadding?: boolean; + className?: string; +}; + +export const ModalFooter = ({ + children, + autoHeight, + centered, + smallPadding, + className, +}: ModalFooterProps) => ( +
    + {children} +
    +); diff --git a/packages/twenty-ui/src/layout/modal/components/ModalHeader.module.scss b/packages/twenty-ui/src/layout/modal/components/ModalHeader.module.scss new file mode 100644 index 0000000000..0a7775e47f --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/ModalHeader.module.scss @@ -0,0 +1,31 @@ +.header { + align-items: center; + display: flex; + flex-direction: row; + flex-shrink: 0; + height: 60px; + overflow: hidden; + padding: var(--t-spacing-5); +} + +.autoHeight { + height: auto; +} + +.noPadding { + padding: 0; +} + +// paddingHorizontal wins over noPadding, like the deprecated Linaria switch +.withHorizontalPadding { + padding: 0 var(--modal-header-padding-horizontal); + + @include respond-to(mobile) { + padding-left: var(--t-spacing-4); + padding-right: var(--t-spacing-4); + } +} + +.withBorderBottom { + border-bottom: 1px solid var(--t-border-color-medium); +} diff --git a/packages/twenty-ui/src/layout/modal/components/ModalHeader.module.scss.d.ts b/packages/twenty-ui/src/layout/modal/components/ModalHeader.module.scss.d.ts new file mode 100644 index 0000000000..abb7431876 --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/ModalHeader.module.scss.d.ts @@ -0,0 +1,8 @@ +declare const classNames: { + readonly header: 'header'; + readonly autoHeight: 'autoHeight'; + readonly noPadding: 'noPadding'; + readonly withHorizontalPadding: 'withHorizontalPadding'; + readonly withBorderBottom: 'withBorderBottom'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/layout/modal/components/ModalHeader.tsx b/packages/twenty-ui/src/layout/modal/components/ModalHeader.tsx new file mode 100644 index 0000000000..5f886965d7 --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/ModalHeader.tsx @@ -0,0 +1,41 @@ +import { clsx } from 'clsx'; + +import styles from './ModalHeader.module.scss'; + +export type ModalHeaderProps = React.PropsWithChildren & { + noPadding?: boolean; + autoHeight?: boolean; + hasBorderBottom?: boolean; + paddingHorizontal?: number; + backgroundColor?: string; +}; + +export const ModalHeader = ({ + children, + noPadding, + autoHeight, + hasBorderBottom, + paddingHorizontal, + backgroundColor, +}: ModalHeaderProps) => ( +
    + {children} +
    +); diff --git a/packages/twenty-ui/src/layout/modal/components/__stories__/Modal.stories.module.scss b/packages/twenty-ui/src/layout/modal/components/__stories__/Modal.stories.module.scss new file mode 100644 index 0000000000..0a70c803f2 --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/__stories__/Modal.stories.module.scss @@ -0,0 +1,7 @@ +.centeredTitle { + text-align: center; +} + +.sectionContainer { + margin-bottom: var(--t-spacing-6); +} diff --git a/packages/twenty-ui/src/layout/modal/components/__stories__/Modal.stories.module.scss.d.ts b/packages/twenty-ui/src/layout/modal/components/__stories__/Modal.stories.module.scss.d.ts new file mode 100644 index 0000000000..d66bb74eaf --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/__stories__/Modal.stories.module.scss.d.ts @@ -0,0 +1,5 @@ +declare const classNames: { + readonly centeredTitle: 'centeredTitle'; + readonly sectionContainer: 'sectionContainer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/layout/modal/components/__stories__/Modal.stories.tsx b/packages/twenty-ui/src/layout/modal/components/__stories__/Modal.stories.tsx new file mode 100644 index 0000000000..a0e5ec16a7 --- /dev/null +++ b/packages/twenty-ui/src/layout/modal/components/__stories__/Modal.stories.tsx @@ -0,0 +1,246 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { useState } from 'react'; +import { H1Title, H1TitleFontColor, H2Title, IconX } from '@ui/display'; +import { Button, IconButton } from '@ui/input'; +import { Section, SectionAlignment, SectionFontColor } from '@ui/layout'; +import { ComponentDecorator } from '@ui/testing'; + +import { Modal } from '../Modal'; +import { ModalContent } from '../ModalContent'; +import { ModalFooter } from '../ModalFooter'; +import { ModalHeader } from '../ModalHeader'; + +import styles from './Modal.stories.module.scss'; + +const meta: Meta = { + title: 'UI/Layout/Modal/Modal', + component: Modal, + decorators: [ComponentDecorator], + argTypes: { + size: { + control: 'select', + options: ['small', 'medium', 'large', 'extraLarge'], + }, + padding: { + control: 'select', + options: ['none', 'small', 'medium', 'large'], + }, + overlay: { + control: 'select', + options: ['light', 'dark', 'transparent'], + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + isOpen: true, + size: 'medium', + padding: 'none', + overlay: 'dark', + }, + render: ({ isOpen, size, padding, overlay }) => ( + + + + + +
    + Workspace name and subdomain can be changed from the settings panel. + These changes will be reflected across all members. +
    +
    + + + + {isNonEmptyString(tooltipContent) && ( + + )} + + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/components/__stories__/MenuPicker.stories.module.scss b/packages/twenty-ui/src/navigation/menu/components/__stories__/MenuPicker.stories.module.scss new file mode 100644 index 0000000000..953fb6c6cb --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/components/__stories__/MenuPicker.stories.module.scss @@ -0,0 +1,6 @@ +.title { + align-items: center; + color: var(--t-font-color-tertiary); + font-size: 12px; + margin-bottom: 8px; +} diff --git a/packages/twenty-ui/src/navigation/menu/components/__stories__/MenuPicker.stories.module.scss.d.ts b/packages/twenty-ui/src/navigation/menu/components/__stories__/MenuPicker.stories.module.scss.d.ts new file mode 100644 index 0000000000..08ba7f87ba --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/components/__stories__/MenuPicker.stories.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly title: 'title'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/menu/components/__stories__/MenuPicker.stories.tsx b/packages/twenty-ui/src/navigation/menu/components/__stories__/MenuPicker.stories.tsx new file mode 100644 index 0000000000..5587673053 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/components/__stories__/MenuPicker.stories.tsx @@ -0,0 +1,115 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { IconChartPie, TooltipDelay } from '@ui/display'; +import { MenuPicker } from '@ui/navigation/menu/components/MenuPicker'; +import { ComponentDecorator } from '@ui/testing'; +import { type ReactNode } from 'react'; + +import styles from './MenuPicker.stories.module.scss'; + +const meta: Meta = { + title: 'UI/Navigation/Menu/MenuPicker', + component: MenuPicker, + decorators: [ComponentDecorator], + args: { + icon: IconChartPie, + label: 'Chart', + selected: false, + disabled: false, + showLabel: true, + }, + argTypes: { + icon: { + control: false, + }, + onClick: { + action: 'clicked', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, +}; + +export const Selected: Story = { + args: { + selected: true, + }, +}; + +export const Disabled: Story = { + args: { + disabled: true, + }, +}; + +export const WithoutLabel: Story = { + args: { + showLabel: false, + }, +}; + +const SectionTitle = ({ children }: { children?: ReactNode }) => { + return

    {children}

    ; +}; + +export const AllStates: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + render: () => ( +
    +
    + Default + +
    +
    + Selected + +
    +
    + Disabled + +
    +
    + No Label + +
    +
    + With Tooltip + +
    +
    + ), +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.module.scss b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.module.scss new file mode 100644 index 0000000000..313fe8a19f --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.module.scss @@ -0,0 +1,13 @@ +// Replaces the deprecated framer-motion rotate animation on the sub-menu +// chevron with an equivalent CSS transform transition. +.subMenuIcon { + display: flex; + align-items: center; + justify-content: center; + transform: rotate(0deg); + transition: transform duration(normal) ease-in-out; +} + +.subMenuIconOpened { + transform: rotate(90deg); +} diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.module.scss.d.ts b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.module.scss.d.ts new file mode 100644 index 0000000000..4e0a52fa2a --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.module.scss.d.ts @@ -0,0 +1,5 @@ +declare const classNames: { + readonly subMenuIcon: 'subMenuIcon'; + readonly subMenuIconOpened: 'subMenuIconOpened'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.tsx new file mode 100644 index 0000000000..6f4dc3d636 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.tsx @@ -0,0 +1,155 @@ +import { IconChevronRight, type IconComponent } from '@ui/display'; +import { type LightIconButtonProps } from '@ui/input/button/components/LightIconButton'; +import { LightIconButtonGroup } from '@ui/input/button/components/LightIconButtonGroup'; +import { + type FunctionComponent, + type MouseEvent, + type ReactElement, + type ReactNode, + useContext, +} from 'react'; + +import { MenuItemHotKeys } from '@ui/navigation/menu/menu-item/components/MenuItemHotKeys'; +import { type ThemeColor } from '@ui/theme'; +import { ThemeContext } from '@ui/theme-constants'; +import { clsx } from 'clsx'; +import { type Nullable } from '@ui/utilities/types/Nullable'; +import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent'; +import { + StyledHoverableMenuItemBase, + StyledMenuItemRightContent, +} from '../internals/components/StyledMenuItemBase'; +import { type MenuItemAccent } from '../types/MenuItemAccent'; + +import styles from './MenuItem.module.scss'; + +export type MenuItemIconButton = { + Wrapper?: FunctionComponent<{ iconButton: ReactElement }>; + Icon: IconComponent; + accent?: LightIconButtonProps['accent']; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onClick?: (event: MouseEvent) => void; + ariaLabel?: string; + dataTestId?: string; +}; + +export type MenuItemProps = { + accent?: MenuItemAccent; + className?: string; + withIconContainer?: boolean; + withIconContainerBackground?: boolean; + iconButtons?: MenuItemIconButton[]; + isIconDisplayedOnHoverOnly?: boolean; + isTooltipOpen?: boolean; + LeftIcon?: IconComponent | null; + iconThemeColor?: ThemeColor | null; + LeftComponent?: ReactNode; + RightIcon?: IconComponent | null; + RightComponent?: ReactNode; + onClick?: (event: MouseEvent) => void; + onMouseEnter?: (event: MouseEvent) => void; + onMouseLeave?: (event: MouseEvent) => void; + testId?: string; + disabled?: boolean; + text: ReactNode; + contextualTextPosition?: 'left' | 'right'; + contextualText?: ReactNode; + hasSubMenu?: boolean; + focused?: boolean; + selected?: boolean; + hotKeys?: Nullable; + isSubMenuOpened?: boolean; +}; + +export const MenuItem = ({ + accent = 'default', + className, + withIconContainer = false, + withIconContainerBackground = true, + iconButtons, + isIconDisplayedOnHoverOnly = true, + LeftIcon, + iconThemeColor, + LeftComponent, + RightIcon, + RightComponent, + onClick, + onMouseEnter, + onMouseLeave, + testId, + text, + contextualTextPosition = 'left', + contextualText, + hasSubMenu = false, + disabled = false, + focused = false, + selected = false, + hotKeys, + isSubMenuOpened = false, +}: MenuItemProps) => { + const { theme } = useContext(ThemeContext); + const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0; + + const handleMenuItemClick = (event: MouseEvent) => { + if (!onClick) return; + event.preventDefault(); + event.stopPropagation(); + + onClick?.(event); + }; + + return ( + + + + + {iconButtons && ( +
    + {showIconButtons && ( + + )} +
    + )} + {hotKeys && } + {RightIcon && ( + + )} + {RightComponent} + {hasSubMenu && ( +
    + +
    + )} +
    +
    + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemAvatar.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemAvatar.tsx new file mode 100644 index 0000000000..920a5f47fd --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemAvatar.tsx @@ -0,0 +1,71 @@ +import { Avatar, type AvatarProps } from '@ui/display'; +import { + type MenuItemIconButton, + MenuItem, +} from '@ui/navigation/menu/menu-item/components/MenuItem'; +import { type MouseEvent, type ReactNode } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; +import { type MenuItemAccent } from '../types/MenuItemAccent'; + +export type MenuItemAvatarProps = { + accent?: MenuItemAccent; + className?: string; + iconButtons?: MenuItemIconButton[]; + isIconDisplayedOnHoverOnly?: boolean; + isTooltipOpen?: boolean; + avatar?: Pick< + AvatarProps, + 'avatarUrl' | 'placeholderColorSeed' | 'placeholder' | 'size' | 'type' + > | null; + onClick?: (event: MouseEvent) => void; + onMouseEnter?: (event: MouseEvent) => void; + onMouseLeave?: (event: MouseEvent) => void; + testId?: string; + text: string; + hasSubMenu?: boolean; + contextualText?: ReactNode; +}; + +export const MenuItemAvatar = ({ + accent = 'default', + className, + iconButtons, + isIconDisplayedOnHoverOnly = true, + isTooltipOpen, + onClick, + onMouseEnter, + onMouseLeave, + testId, + avatar, + hasSubMenu = false, + text, + contextualText, +}: MenuItemAvatarProps) => { + return ( + + ) : undefined + } + onClick={onClick} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + testId={testId} + text={text} + contextualText={contextualText} + hasSubMenu={hasSubMenu} + /> + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemDraggable.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemDraggable.tsx new file mode 100644 index 0000000000..b6e7b3db89 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemDraggable.tsx @@ -0,0 +1,69 @@ +import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent'; +import { StyledHoverableMenuItemBase } from '../internals/components/StyledMenuItemBase'; +import { type MenuItemAccent } from '../types/MenuItemAccent'; + +import { type IconComponent } from '@ui/display'; +import { LightIconButtonGroup } from '@ui/input'; +import { type ReactNode } from 'react'; +import { type MenuItemDraggableGripMode } from '../types/MenuItemDraggableGripMode'; +import { type MenuItemIconButton } from './MenuItem'; + +export type MenuItemDraggableProps = { + LeftIcon?: IconComponent | undefined; + withIconContainer?: boolean; + accent?: MenuItemAccent; + iconButtons?: MenuItemIconButton[]; + isTooltipOpen?: boolean; + onClick?: () => void; + text: ReactNode; + contextualText?: ReactNode; + className?: string; + isIconDisplayedOnHoverOnly?: boolean; + gripMode?: MenuItemDraggableGripMode; + isDragDisabled?: boolean; + isHoverDisabled?: boolean; +}; + +export const MenuItemDraggable = ({ + LeftIcon, + withIconContainer = false, + accent = 'default', + iconButtons, + onClick, + text, + contextualText, + isDragDisabled = false, + className, + isIconDisplayedOnHoverOnly = true, + gripMode = 'never', +}: MenuItemDraggableProps) => { + const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0; + + const cursorType = + gripMode !== 'never' ? (isDragDisabled ? 'default' : 'drag') : 'default'; + + return ( + + + {showIconButtons && ( + + )} + + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemHotKeys.module.scss b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemHotKeys.module.scss new file mode 100644 index 0000000000..bd2383bd27 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemHotKeys.module.scss @@ -0,0 +1,28 @@ +.commandTextContainer { + align-items: center; + display: flex; + flex-direction: row; + gap: var(--t-spacing-1); + justify-content: center; +} + +.commandText { + color: var(--t-font-color-light); + vertical-align: middle; + white-space: nowrap; +} + +.commandKey { + box-sizing: content-box; + align-items: center; + background-color: var(--t-background-secondary); + border: 1px solid var(--t-border-color-strong); + border-radius: var(--t-border-radius-sm); + box-shadow: var(--t-box-shadow-underline); + display: flex; + flex-direction: column; + height: 18px; + justify-content: center; + text-align: center; + width: var(--t-spacing-4); +} diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemHotKeys.module.scss.d.ts b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemHotKeys.module.scss.d.ts new file mode 100644 index 0000000000..1488ef1ec6 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemHotKeys.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly commandTextContainer: 'commandTextContainer'; + readonly commandText: 'commandText'; + readonly commandKey: 'commandKey'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemHotKeys.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemHotKeys.tsx new file mode 100644 index 0000000000..3d8bf74052 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemHotKeys.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import styles from './MenuItemHotKeys.module.scss'; + +export type MenuItemHotKeysProps = { + hotKeys?: string[]; + joinLabel?: string; +}; + +export const MenuItemHotKeys = ({ + hotKeys, + joinLabel = 'then', +}: MenuItemHotKeysProps) => { + return ( +
    + {hotKeys && ( +
    + {hotKeys.map((hotKey, index) => ( + +
    {hotKey}
    + {index < hotKeys.length - 1 && joinLabel} +
    + ))} +
    + )} +
    + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelect.module.scss b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelect.module.scss new file mode 100644 index 0000000000..b097b1aa43 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelect.module.scss @@ -0,0 +1,8 @@ +.leftContentWithCheckboxContainer { + align-items: center; + display: flex; + flex-direction: row; + gap: var(--t-spacing-2); + min-width: 0; + overflow: hidden; +} diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelect.module.scss.d.ts b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelect.module.scss.d.ts new file mode 100644 index 0000000000..4e8d6662d7 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelect.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly leftContentWithCheckboxContainer: 'leftContentWithCheckboxContainer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelect.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelect.tsx new file mode 100644 index 0000000000..a1b1b8cc9c --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelect.tsx @@ -0,0 +1,58 @@ +import { Tag } from '@ui/components'; +import { type IconComponent } from '@ui/display'; +import { Checkbox } from '@ui/input/components/Checkbox'; +import { MenuItemLeftContent } from '@ui/navigation/menu/menu-item/internals/components/MenuItemLeftContent'; +import { type ThemeColor } from '@ui/theme'; +import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase'; + +import styles from './MenuItemMultiSelect.module.scss'; + +type MenuItemMultiSelectProps = { + color?: ThemeColor; + LeftIcon?: IconComponent; + iconThemeColor?: ThemeColor | null; + selected: boolean; + isKeySelected?: boolean; + withIconContainer?: boolean; + text: string; + className: string; + onSelectChange?: (selected: boolean) => void; +}; + +export const MenuItemMultiSelect = ({ + color, + LeftIcon, + iconThemeColor, + withIconContainer = false, + text, + selected, + isKeySelected, + className, + onSelectChange, +}: MenuItemMultiSelectProps) => { + const handleOnClick = () => { + onSelectChange?.(!selected); + }; + + return ( + +
    + + {color ? ( + + ) : ( + + )} +
    +
    + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelectAvatar.module.scss b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelectAvatar.module.scss new file mode 100644 index 0000000000..db185a4942 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelectAvatar.module.scss @@ -0,0 +1,17 @@ +.leftContentWithCheckboxContainer { + align-items: center; + display: flex; + flex-direction: row; + gap: var(--t-spacing-2); + width: 100%; +} + +.textContainer { + display: flex; + align-items: center; + flex: 1 0 0; + gap: var(--t-spacing-1); + max-width: 100%; + text-overflow: ellipsis; + overflow: hidden; +} diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelectAvatar.module.scss.d.ts b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelectAvatar.module.scss.d.ts new file mode 100644 index 0000000000..99da975652 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelectAvatar.module.scss.d.ts @@ -0,0 +1,5 @@ +declare const classNames: { + readonly leftContentWithCheckboxContainer: 'leftContentWithCheckboxContainer'; + readonly textContainer: 'textContainer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelectAvatar.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelectAvatar.tsx new file mode 100644 index 0000000000..bbbc368d78 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelectAvatar.tsx @@ -0,0 +1,64 @@ +import { type ReactNode } from 'react'; + +import { OverflowingTextWithTooltip } from '@ui/display'; +import { Checkbox } from '@ui/input/components/Checkbox'; +import { + StyledMenuItemBase, + StyledMenuItemLabel, + StyledMenuItemLabelLight, + StyledMenuItemLeftContent, +} from '../internals/components/StyledMenuItemBase'; + +import styles from './MenuItemMultiSelectAvatar.module.scss'; + +type MenuItemMultiSelectAvatarProps = { + avatar?: ReactNode; + selected: boolean; + isKeySelected?: boolean; + text?: string; + contextualText?: string; + className?: string; + onSelectChange?: (selected: boolean) => void; +}; + +export const MenuItemMultiSelectAvatar = ({ + avatar, + text, + selected, + contextualText, + className, + isKeySelected, + onSelectChange, +}: MenuItemMultiSelectAvatarProps) => { + const handleOnClick = () => { + onSelectChange?.(!selected); + }; + + return ( + +
    + + + {avatar} +
    + + + + {contextualText && ( + <> + · + + + + + )} +
    +
    +
    +
    + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelectTag.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelectTag.tsx new file mode 100644 index 0000000000..0f7e7b6b05 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelectTag.tsx @@ -0,0 +1,45 @@ +import { Tag } from '@ui/components'; +import { type IconComponent } from '@ui/display'; +import { Checkbox, CheckboxShape, CheckboxSize } from '@ui/input'; +import { type ThemeColor } from '@ui/theme'; +import { + StyledMenuItemBase, + StyledMenuItemLeftContent, +} from '../internals/components/StyledMenuItemBase'; + +type MenuItemMultiSelectTagProps = { + selected: boolean; + className?: string; + isKeySelected?: boolean; + onClick?: () => void; + color: ThemeColor | 'transparent'; + text: string; + Icon?: IconComponent; +}; + +export const MenuItemMultiSelectTag = ({ + color, + selected, + className, + onClick, + isKeySelected, + text, + Icon, +}: MenuItemMultiSelectTagProps) => { + return ( + + + + + + + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemNavigate.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemNavigate.tsx new file mode 100644 index 0000000000..1bd3416ae9 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemNavigate.tsx @@ -0,0 +1,42 @@ +import { IconChevronRight, type IconComponent } from '@ui/display'; +import { ThemeContext } from '@ui/theme-constants'; +import { useContext } from 'react'; +import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent'; +import { + StyledMenuItemBase, + StyledMenuItemLeftContent, +} from '../internals/components/StyledMenuItemBase'; + +export type MenuItemNavigateProps = { + LeftIcon?: IconComponent; + withIconContainer?: boolean; + text: string; + onClick?: () => void; + className?: string; +}; + +export const MenuItemNavigate = ({ + LeftIcon, + withIconContainer = false, + text, + className, + onClick, +}: MenuItemNavigateProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + + + + + + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelect.module.scss b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelect.module.scss new file mode 100644 index 0000000000..02951ef48c --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelect.module.scss @@ -0,0 +1,43 @@ +.menuItemSelect { + --horizontal-padding: var(--t-spacing-1); + --vertical-padding: var(--t-spacing-2); + + // Height and width below are sized against the content box. + box-sizing: content-box; + align-items: center; + border-radius: var(--t-border-radius-sm); + display: flex; + flex-direction: row; + font-size: var(--t-font-size-sm); + gap: var(--t-spacing-2); + height: calc(32px - 2 * var(--vertical-padding)); + justify-content: space-between; + padding: var(--vertical-padding) var(--horizontal-padding); + position: relative; + user-select: none; + width: calc(100% - 2 * var(--horizontal-padding)); + transition: background 0.1s ease; + color: var(--t-font-color-secondary); + cursor: pointer; +} + +.menuItemSelect[data-focused], +.menuItemSelect[data-key-selected] { + background: var(--t-background-transparent-light); +} + +.menuItemSelect:hover { + background: var(--t-background-transparent-light); +} + +// Disabled wins over focused/key-selected/hover, mirroring the deprecated +// Linaria ternary order. +.menuItemSelect[data-disabled] { + background: inherit; + color: var(--t-font-color-tertiary); + cursor: default; +} + +.menuItemSelect[data-disabled]:hover { + background: inherit; +} diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelect.module.scss.d.ts b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelect.module.scss.d.ts new file mode 100644 index 0000000000..eaf7bc9789 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelect.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly menuItemSelect: 'menuItemSelect'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelect.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelect.tsx new file mode 100644 index 0000000000..f4da3e2749 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelect.tsx @@ -0,0 +1,129 @@ +import { isString } from '@sniptt/guards'; +import { + IconCheck, + IconChevronRight, + OverflowingTextWithTooltip, + type IconComponent, +} from '@ui/display'; +import { type ThemeColor } from '@ui/theme'; +import { ThemeContext } from '@ui/theme-constants'; +import { clsx } from 'clsx'; +import { + type ComponentPropsWithoutRef, + forwardRef, + useContext, + type ReactNode, +} from 'react'; +import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent'; +import { + StyledMenuItemLabel, + StyledMenuItemRightContent, + StyledRightMenuItemContextualText, +} from '../internals/components/StyledMenuItemBase'; + +import styles from './MenuItemSelect.module.scss'; + +// The deprecated Linaria styled component forwarded refs and arbitrary +// native props, so the port preserves that contract. +type StyledMenuItemSelectProps = { + disabled?: boolean; + focused?: boolean; + isKeySelected?: boolean; +} & ComponentPropsWithoutRef<'div'>; + +export const StyledMenuItemSelect = forwardRef< + HTMLDivElement, + StyledMenuItemSelectProps +>(({ disabled, focused, isKeySelected, className, children, ...rest }, ref) => ( +
    + {children} +
    +)); + +StyledMenuItemSelect.displayName = 'StyledMenuItemSelect'; + +type MenuItemSelectProps = { + LeftIcon?: IconComponent | null | undefined; + leftIconColor?: ThemeColor | null; + withIconContainer?: boolean; + selected: boolean; + needIconCheck?: boolean; + text: string; + className?: string; + onClick?: () => void; + disabled?: boolean; + focused?: boolean; + hasSubMenu?: boolean; + contextualText?: ReactNode; + contextualTextPosition?: 'left' | 'right'; +}; + +export const MenuItemSelect = ({ + LeftIcon, + leftIconColor, + withIconContainer = false, + text, + selected, + needIconCheck = true, + className, + onClick, + disabled, + focused, + hasSubMenu = false, + contextualText, + contextualTextPosition = 'left', +}: MenuItemSelectProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + + + {contextualTextPosition === 'right' && ( + + {isString(contextualText) ? ( + + + + ) : ( + contextualText + )} + + )} + + {selected && needIconCheck && } + + {hasSubMenu && ( + + )} + + + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelectAvatar.module.scss b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelectAvatar.module.scss new file mode 100644 index 0000000000..3364391087 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelectAvatar.module.scss @@ -0,0 +1,9 @@ +.textContainer { + display: flex; + align-items: center; + flex: 1 0 0; + gap: var(--t-spacing-1); + max-width: 100%; + text-overflow: ellipsis; + overflow: hidden; +} diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelectAvatar.module.scss.d.ts b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelectAvatar.module.scss.d.ts new file mode 100644 index 0000000000..6ae17db92a --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelectAvatar.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly textContainer: 'textContainer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelectAvatar.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelectAvatar.tsx new file mode 100644 index 0000000000..95083ae71b --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelectAvatar.tsx @@ -0,0 +1,71 @@ +import { type ReactNode, useContext } from 'react'; + +import { + StyledMenuItemIconCheck, + StyledMenuItemLabel, + StyledMenuItemLabelLight, + StyledMenuItemLeftContent, +} from '../internals/components/StyledMenuItemBase'; + +import { OverflowingTextWithTooltip } from '@ui/display'; +import { ThemeContext } from '@ui/theme-constants'; +import { StyledMenuItemSelect } from './MenuItemSelect'; + +import styles from './MenuItemSelectAvatar.module.scss'; + +type MenuItemSelectAvatarProps = { + avatar?: ReactNode; + selected: boolean; + text: string; + contextualText?: string; + className?: string; + onClick?: (event?: React.MouseEvent) => void; + disabled?: boolean; + focused?: boolean; + testId?: string; +}; + +export const MenuItemSelectAvatar = ({ + avatar, + text, + contextualText, + selected, + className, + onClick, + disabled, + focused, + testId, +}: MenuItemSelectAvatarProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + + {avatar} +
    + + + + {contextualText && ( + <> + · + + + + + )} +
    +
    + {selected && } +
    + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelectColor.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelectColor.tsx new file mode 100644 index 0000000000..7abb018fd0 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelectColor.tsx @@ -0,0 +1,54 @@ +import { + StyledMenuItemIconCheck, + StyledMenuItemLabel, + StyledMenuItemLeftContent, +} from '../internals/components/StyledMenuItemBase'; + +import { ColorSample, type ColorSampleVariant } from '@ui/display'; +import { type ThemeColor } from '@ui/theme'; +import { ThemeContext } from '@ui/theme-constants'; +import { useContext } from 'react'; +import { + DEFAULT_COLOR_LABELS, + type ColorLabels, +} from '../constants/DefaultColorLabels'; +import { StyledMenuItemSelect } from './MenuItemSelect'; + +type MenuItemSelectColorProps = { + selected: boolean; + className?: string; + onClick?: () => void; + disabled?: boolean; + focused?: boolean; + color: ThemeColor; + variant?: ColorSampleVariant; + colorLabels?: ColorLabels; +}; + +export const MenuItemSelectColor = ({ + color, + selected, + className, + onClick, + disabled, + focused, + variant = 'default', + colorLabels = DEFAULT_COLOR_LABELS, +}: MenuItemSelectColorProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + + + {colorLabels[color]} + + {selected && } + + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelectTag.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelectTag.tsx new file mode 100644 index 0000000000..e28e34505f --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSelectTag.tsx @@ -0,0 +1,56 @@ +import { + StyledMenuItemIconCheck, + StyledMenuItemLeftContent, +} from '../internals/components/StyledMenuItemBase'; + +import { Tag } from '@ui/components'; +import { type IconComponent } from '@ui/display'; +import { type ThemeColor } from '@ui/theme'; +import { ThemeContext } from '@ui/theme-constants'; +import { useContext } from 'react'; +import { StyledMenuItemSelect } from './MenuItemSelect'; + +type MenuItemSelectTagProps = { + selected?: boolean; + focused?: boolean; + isKeySelected?: boolean; + className?: string; + onClick?: () => void; + color: ThemeColor | 'transparent'; + text: string; + variant?: 'solid' | 'outline'; + LeftIcon?: IconComponent | null; +}; + +export const MenuItemSelectTag = ({ + color, + selected, + focused, + isKeySelected, + className, + onClick, + text, + variant = 'solid', + LeftIcon, +}: MenuItemSelectTagProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + + + + {selected && } + + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.module.scss b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.module.scss new file mode 100644 index 0000000000..e2cde3dba7 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.module.scss @@ -0,0 +1,31 @@ +.suggestionMenuItem { + --horizontal-padding: var(--t-spacing-1); + --vertical-padding: var(--t-spacing-2); + + // Height below is sized against the content box. + box-sizing: content-box; + align-items: center; + border-radius: var(--t-border-radius-sm); + cursor: pointer; + display: flex; + flex-direction: row; + font-size: var(--t-font-size-sm); + gap: var(--t-spacing-2); + height: calc(32px - 2 * var(--vertical-padding)); + justify-content: space-between; + padding: var(--vertical-padding) var(--horizontal-padding); + background: transparent; + color: var(--t-font-color-secondary); + transition: background 0.1s ease; + position: relative; + user-select: none; + width: calc(100% - 2 * var(--horizontal-padding)); +} + +.suggestionMenuItem[data-selected] { + background: var(--t-background-transparent-medium); +} + +.suggestionMenuItem:hover { + background: var(--t-background-transparent-light); +} diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.module.scss.d.ts b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.module.scss.d.ts new file mode 100644 index 0000000000..c4bfd7e099 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly suggestionMenuItem: 'suggestionMenuItem'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.tsx new file mode 100644 index 0000000000..ad80e78e53 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.tsx @@ -0,0 +1,56 @@ +import { clsx } from 'clsx'; +import { type MouseEvent } from 'react'; + +import { type IconComponent } from '@ui/display'; +import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent'; +import { StyledMenuItemLeftContent } from '../internals/components/StyledMenuItemBase'; + +import styles from './MenuItemSuggestion.module.scss'; + +export type MenuItemSuggestionProps = { + LeftIcon?: IconComponent | null; + withIconContainer?: boolean; + text: string; + contextualText?: string; + contextualTextPosition?: 'left' | 'right'; + selected?: boolean; + className?: string; + onClick?: (event: MouseEvent) => void; +}; + +export const MenuItemSuggestion = ({ + LeftIcon, + withIconContainer = false, + text, + contextualText = undefined, + contextualTextPosition = 'left', + className, + selected, + onClick, +}: MenuItemSuggestionProps) => { + const handleMenuItemClick = (event: MouseEvent) => { + if (!onClick) return; + event.preventDefault(); + event.stopPropagation(); + + onClick?.(event); + }; + + return ( +
  • + + + +
  • + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.module.scss b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.module.scss new file mode 100644 index 0000000000..d56463b176 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.module.scss @@ -0,0 +1,7 @@ +.toggleContainer { + align-items: center; + cursor: pointer; + display: flex; + justify-content: space-between; + width: 100%; +} diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.module.scss.d.ts b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.module.scss.d.ts new file mode 100644 index 0000000000..8d31b11da5 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly toggleContainer: 'toggleContainer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.tsx new file mode 100644 index 0000000000..9a65a7524e --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.tsx @@ -0,0 +1,65 @@ +import { type IconComponent } from '@ui/display'; +import { Toggle, type ToggleSize } from '@ui/input'; +import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent'; +import { + StyledMenuItemBase, + StyledMenuItemRightContent, +} from '../internals/components/StyledMenuItemBase'; + +import styles from './MenuItemToggle.module.scss'; + +export type MenuItemToggleProps = { + focused?: boolean; + LeftIcon?: IconComponent; + withIconContainer?: boolean; + toggled: boolean; + text: string; + className?: string; + onToggleChange?: (toggled: boolean) => void; + toggleSize?: ToggleSize; + disabled?: boolean; +}; + +export const MenuItemToggle = ({ + focused, + LeftIcon, + withIconContainer = false, + text, + toggled, + className, + onToggleChange, + toggleSize, + disabled = false, +}: MenuItemToggleProps) => { + const handleClick = () => { + if (!disabled) { + onToggleChange?.(!toggled); + } + }; + + return ( + +
    + + e.stopPropagation()}> + + +
    +
    + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItem.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItem.stories.tsx new file mode 100644 index 0000000000..2fc2f7761c --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItem.stories.tsx @@ -0,0 +1,325 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { action } from 'storybook/actions'; + +import { IconBell } from '@ui/display'; +import { + CatalogDecorator, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; +import { type MenuItemAccent } from '../../types/MenuItemAccent'; +import { MenuItem } from '../MenuItem'; + +const meta: Meta = { + title: 'UI/Navigation/Menu/MenuItem/MenuItem', + component: MenuItem, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + text: 'Menu item text', + LeftIcon: IconBell, + accent: 'default', + iconButtons: [ + { Icon: IconBell, onClick: action('Clicked') }, + { Icon: IconBell, onClick: action('Clicked') }, + ], + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: { ...Default.args }, + argTypes: { + accent: { control: false }, + className: { control: false }, + iconButtons: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, + catalog: { + dimensions: [ + { + name: 'withIcon', + values: [true, false], + props: (withIcon: boolean) => ({ + LeftIcon: withIcon ? IconBell : undefined, + }), + labels: (withIcon: boolean) => + withIcon ? 'With left icon' : 'Without left icon', + }, + { + name: 'withIconContainer', + values: [true, false], + props: (withIconContainer: boolean) => ({ withIconContainer }), + labels: (withIconContainer: boolean) => + withIconContainer + ? 'With icon container' + : 'Without icon container', + }, + { + name: 'accents', + values: ['default', 'danger'] satisfies MenuItemAccent[], + props: (accent: MenuItemAccent) => ({ accent }), + }, + { + name: 'states', + values: ['default', 'hover'], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + return { className: state }; + default: + return {}; + } + }, + }, + { + name: 'iconButtons', + values: ['no icon button', 'two icon buttons'], + props: (choice: string) => { + switch (choice) { + case 'no icon button': { + return { + iconButtons: [], + }; + } + case 'two icon buttons': { + return { + iconButtons: [ + { + Icon: IconBell, + onClick: action('Clicked on first icon button'), + }, + { + Icon: IconBell, + onClick: action('Clicked on second icon button'), + }, + ], + }; + } + default: + return {}; + } + }, + }, + { + name: 'disabled', + values: [true, false], + props: (disabled: boolean) => ({ disabled }), + }, + ], + options: { + elementContainer: { + width: 200, + }, + }, + }, + }, + decorators: [CatalogDecorator], +}; + +export const HotKeysCatalog: CatalogStory = { + args: { + text: 'Menu item with hotkeys', + LeftIcon: IconBell, + hotKeys: ['⌘', 'K'], + }, + argTypes: { + className: { control: false }, + iconButtons: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, + catalog: { + dimensions: [ + { + name: 'hotKeyTypes', + values: ['no hotkeys', 'single key', 'modifier + key'], + props: (choice: string) => { + switch (choice) { + case 'no hotkeys': + return { hotKeys: undefined }; + case 'single key': + return { hotKeys: ['K'] }; + case 'modifier + key': + return { hotKeys: ['⌘', 'K'] }; + default: + return {}; + } + }, + labels: (choice: string) => { + switch (choice) { + case 'no hotkeys': + return 'No hotkeys'; + case 'single key': + return 'Single key (K)'; + case 'modifier + key': + return 'Modifier + key (⌘K)'; + default: + return choice; + } + }, + }, + { + name: 'states', + values: ['default', 'hover'], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + return { className: 'hover' }; + default: + return {}; + } + }, + }, + ], + options: { + elementContainer: { + width: 300, + }, + }, + }, + }, + decorators: [CatalogDecorator], +}; + +export const ContextualTextCatalog: CatalogStory = { + args: { + text: 'Menu item with contextual text', + }, + decorators: [CatalogDecorator], + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, + catalog: { + dimensions: [ + { + name: 'contextualTextPosition', + values: ['left', 'right'], + props: (contextualTextPosition: 'left' | 'right') => ({ + contextualTextPosition, + }), + labels: (contextualTextPosition: 'left' | 'right') => + contextualTextPosition === 'left' ? 'Left' : 'Right', + }, + { + name: 'contextualText', + values: [ + 'Contextual text', + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Eveniet, corporis.', + ], + props: (contextualText: string) => ({ contextualText }), + labels: (contextualText: string) => { + switch (contextualText) { + case 'Contextual text': + return 'Contextual text'; + case 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Eveniet, corporis.': + return 'Long contextual text'; + default: + return contextualText; + } + }, + }, + { + name: 'iconButtons', + values: ['no icon button', 'one icon button'], + props: (choice: string) => { + switch (choice) { + case 'no icon button': { + return { + iconButtons: [], + }; + } + case 'one icon button': { + return { + iconButtons: [ + { + Icon: IconBell, + onClick: action('Clicked on icon button'), + }, + ], + }; + } + default: + return {}; + } + }, + labels: (choice: string) => { + switch (choice) { + case 'no icon button': + return 'No icon button'; + case 'one icon button': + return 'One icon button'; + default: + return choice; + } + }, + }, + { + name: 'states', + values: ['default', 'hover'], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + return { className: 'hover' }; + default: + return {}; + } + }, + }, + ], + options: { + elementContainer: { + style: { width: '400px', overflow: 'hidden' }, + }, + }, + }, + }, +}; + +export const SubMenuCatalog: CatalogStory = { + args: { + text: 'Menu item with sub menu', + LeftIcon: IconBell, + hasSubMenu: true, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + catalog: { + dimensions: [ + { + name: 'isSubMenuOpened', + values: [true, false], + props: (isSubMenuOpened: boolean) => ({ isSubMenuOpened }), + labels: (isSubMenuOpened: boolean) => + isSubMenuOpened ? 'Opened' : 'Closed', + }, + ], + options: { + elementContainer: { + width: 200, + }, + }, + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemDraggable.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemDraggable.stories.tsx new file mode 100644 index 0000000000..fbb9706c97 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemDraggable.stories.tsx @@ -0,0 +1,147 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { action } from 'storybook/actions'; + +import { IconBell, IconMinus } from '@ui/display'; +import { + CatalogDecorator, + type CatalogDimension, + type CatalogOptions, + ComponentDecorator, +} from '@ui/testing'; +import { type MenuItemAccent } from '../../types/MenuItemAccent'; +import { MenuItemDraggable } from '../MenuItemDraggable'; + +const meta: Meta = { + title: 'UI/Navigation/Menu/MenuItem/MenuItemDraggable', + component: MenuItemDraggable, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + LeftIcon: IconBell, + accent: 'default', + iconButtons: [{ Icon: IconMinus, onClick: action('Clicked') }], + onClick: action('Clicked'), + text: 'Menu item draggable', + isDragDisabled: false, + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: Story = { + args: { ...Default.args }, + argTypes: { + accent: { control: false }, + iconButtons: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'] }, + catalog: { + dimensions: [ + { + name: 'withIconContainer', + values: [true, false], + props: (withIconContainer: boolean) => ({ withIconContainer }), + labels: (withIconContainer: boolean) => + withIconContainer + ? 'With icon container' + : 'Without icon container', + }, + { + name: 'isDragDisabled', + values: [true, false], + props: (isDragDisabled: boolean) => ({ + isDragDisabled: isDragDisabled, + }), + labels: (isDragDisabled: boolean) => + isDragDisabled ? 'Without drag icon' : 'With drag icon', + }, + { + name: 'accents', + values: ['default', 'danger', 'placeholder'] as MenuItemAccent[], + props: (accent: MenuItemAccent) => ({ accent }), + }, + { + name: 'states', + values: ['default', 'hover'], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + return { className: state }; + default: + return {}; + } + }, + }, + { + name: 'iconButtons', + values: ['no icon button', 'minus icon buttons'], + props: (choice: string) => { + switch (choice) { + case 'no icon button': { + return { + iconButtons: [], + }; + } + case 'minus icon buttons': { + return { + iconButtons: [ + { + Icon: IconMinus, + onClick: action('Clicked on minus icon button'), + }, + ], + }; + } + } + }, + }, + ] as CatalogDimension[], + options: { + elementContainer: { + width: 200, + }, + } as CatalogOptions, + }, + }, + decorators: [CatalogDecorator], +}; + +export const Grip: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { ...Default.args, gripMode: 'always', isDragDisabled: false }, +}; + +export const GripOnHover: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { ...Default.args, gripMode: 'onHover', isDragDisabled: false }, +}; + +export const GripOnHoverWithIconContainer: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + ...Default.args, + gripMode: 'onHover', + withIconContainer: true, + isDragDisabled: false, + }, +}; + +export const HoverDisabled: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { ...Default.args, isHoverDisabled: true }, +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemMultiSelect.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemMultiSelect.stories.tsx new file mode 100644 index 0000000000..7a75ecbbce --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemMultiSelect.stories.tsx @@ -0,0 +1,90 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { IconBell } from '@ui/display'; +import { + CatalogDecorator, + type CatalogDimension, + type CatalogOptions, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; +import { MenuItemMultiSelect } from '../MenuItemMultiSelect'; + +const meta: Meta = { + title: 'UI/Navigation/Menu/MenuItem/MenuItemMultiSelect', + component: MenuItemMultiSelect, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + text: 'First option', + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: { LeftIcon: IconBell, text: 'Menu item' }, + argTypes: { + className: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, + catalog: { + dimensions: [ + { + name: 'withIcon', + values: [true, false], + props: (withIcon: boolean) => ({ + LeftIcon: withIcon ? IconBell : undefined, + }), + labels: (withIcon: boolean) => + withIcon ? 'With left icon' : 'Without left icon', + }, + { + name: 'withIconContainer', + values: [true, false], + props: (withIconContainer: boolean) => ({ withIconContainer }), + labels: (withIconContainer: boolean) => + withIconContainer + ? 'With icon container' + : 'Without icon container', + }, + { + name: 'selected', + values: [true, false], + props: (selected: boolean) => ({ selected }), + labels: (selected: boolean) => + selected ? 'Selected' : 'Not selected', + }, + { + name: 'states', + values: ['default', 'hover'], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + return { className: state }; + default: + return {}; + } + }, + }, + ] as CatalogDimension[], + options: { + elementContainer: { + width: 200, + }, + } as CatalogOptions, + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemMultiSelectAvatar.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemMultiSelectAvatar.stories.tsx new file mode 100644 index 0000000000..cd1dce7de6 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemMultiSelectAvatar.stories.tsx @@ -0,0 +1,89 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { Avatar } from '@ui/display'; +import { + AVATAR_URL_MOCK, + CatalogDecorator, + type CatalogDimension, + type CatalogOptions, + type CatalogStory, + ComponentDecorator, + JotaiRootDecorator, +} from '@ui/testing'; +import { MenuItemMultiSelectAvatar } from '../MenuItemMultiSelectAvatar'; + +const meta: Meta = { + title: 'UI/Navigation/Menu/MenuItem/MenuItemMultiSelectAvatar', + component: MenuItemMultiSelectAvatar, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + text: 'First option', + contextualText: 'Contextual text', + avatar: , + }, + decorators: [ComponentDecorator, JotaiRootDecorator], +}; + +export const Catalog: CatalogStory = { + args: { text: 'Menu item' }, + argTypes: { + className: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, + catalog: { + dimensions: [ + { + name: 'withAvatar', + values: [true, false], + props: (withAvatar: boolean) => ({ + avatar: withAvatar ? ( + + ) : ( + + ), + }), + labels: (withAvatar: boolean) => + withAvatar ? 'With avatar' : 'Without avatar', + }, + { + name: 'selected', + values: [true, false], + props: (selected: boolean) => ({ selected }), + labels: (selected: boolean) => + selected ? 'Selected' : 'Not selected', + }, + { + name: 'states', + values: ['default', 'hover'], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + return { className: state }; + default: + return {}; + } + }, + }, + ] as CatalogDimension[], + options: { + elementContainer: { + width: 200, + }, + } as CatalogOptions, + }, + }, + decorators: [CatalogDecorator, JotaiRootDecorator], +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemNavigate.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemNavigate.stories.tsx new file mode 100644 index 0000000000..771b096de2 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemNavigate.stories.tsx @@ -0,0 +1,82 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { IconBell } from '@ui/display'; +import { MenuItemNavigate } from '../MenuItemNavigate'; + +import { + CatalogDecorator, + type CatalogDimension, + type CatalogOptions, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; + +const meta: Meta = { + title: 'UI/Navigation/Menu/MenuItem/MenuItemNavigate', + component: MenuItemNavigate, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + text: 'First option', + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: { LeftIcon: IconBell, text: 'Menu item' }, + argTypes: { + className: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, + catalog: { + dimensions: [ + { + name: 'withIcon', + values: [true, false], + props: (withIcon: boolean) => ({ + LeftIcon: withIcon ? IconBell : undefined, + }), + labels: (withIcon: boolean) => + withIcon ? 'With left icon' : 'Without left icon', + }, + { + name: 'withIconContainer', + values: [true, false], + props: (withIconContainer: boolean) => ({ withIconContainer }), + labels: (withIconContainer: boolean) => + withIconContainer + ? 'With icon container' + : 'Without icon container', + }, + { + name: 'states', + values: ['default', 'hover'], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + return { className: state }; + default: + return {}; + } + }, + }, + ] as CatalogDimension[], + options: { + elementContainer: { + width: 200, + }, + } as CatalogOptions, + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelect.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelect.stories.tsx new file mode 100644 index 0000000000..79405883d4 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelect.stories.tsx @@ -0,0 +1,91 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { + CatalogDecorator, + type CatalogDimension, + type CatalogOptions, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; +import { MenuItemSelect } from '../MenuItemSelect'; + +import { IconBell } from '@ui/display'; +const meta: Meta = { + title: 'UI/Navigation/Menu/MenuItem/MenuItemSelect', + component: MenuItemSelect, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + text: 'First option', + LeftIcon: IconBell, + }, + argTypes: { + className: { control: false }, + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: { LeftIcon: IconBell, text: 'Menu item' }, + argTypes: { + className: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, + catalog: { + dimensions: [ + { + name: 'withIcon', + values: [true, false], + props: (withIcon: boolean) => ({ + LeftIcon: withIcon ? IconBell : undefined, + }), + labels: (withIcon: boolean) => + withIcon ? 'With left icon' : 'Without left icon', + }, + { + name: 'withIconContainer', + values: [true, false], + props: (withIconContainer: boolean) => ({ withIconContainer }), + labels: (withIconContainer: boolean) => + withIconContainer + ? 'With icon container' + : 'Without icon container', + }, + { + name: 'states', + values: ['default', 'hover', 'focused', 'hover+focused'], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + return { className: 'hover' }; + case 'focused': + return { focused: true }; + case 'hover+focused': + return { className: 'hover', focused: true }; + default: + return {}; + } + }, + }, + ] as CatalogDimension[], + options: { + elementContainer: { + width: 200, + }, + } as CatalogOptions, + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectAvatar.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectAvatar.stories.tsx new file mode 100644 index 0000000000..5d2257003e --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectAvatar.stories.tsx @@ -0,0 +1,97 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { Avatar } from '@ui/display'; +import { + AVATAR_URL_MOCK, + CatalogDecorator, + type CatalogDimension, + type CatalogOptions, + type CatalogStory, + ComponentDecorator, + JotaiRootDecorator, +} from '@ui/testing'; +import { MenuItemSelectAvatar } from '../MenuItemSelectAvatar'; + +const meta: Meta = { + title: 'UI/Navigation/Menu/MenuItem/MenuItemSelectAvatar', + component: MenuItemSelectAvatar, +}; + +export default meta; + +type Story = StoryObj; +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + text: 'First option', + contextualText: 'Contextual text', + avatar: , + }, + argTypes: { + className: { control: false }, + }, + decorators: [ComponentDecorator, JotaiRootDecorator], +}; + +export const Catalog: CatalogStory = { + args: { text: 'Menu item' }, + argTypes: { + className: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, + catalog: { + dimensions: [ + { + name: 'withAvatar', + values: [true, false], + props: (withAvatar: boolean) => ({ + avatar: withAvatar ? ( + + ) : ( + + ), + }), + labels: (withAvatar: boolean) => + withAvatar ? 'With avatar' : 'Without avatar', + }, + { + name: 'states', + values: [ + 'default', + 'hover', + 'disabled', + 'selected', + 'hover+selected', + ], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + return { className: 'hover' }; + case 'disabled': + return { disabled: true }; + case 'selected': + return { selected: true }; + + case 'hover+selected': + return { className: 'hover', selected: true }; + default: + return {}; + } + }, + }, + ] as CatalogDimension[], + options: { + elementContainer: { + width: 200, + }, + } as CatalogOptions, + }, + }, + decorators: [CatalogDecorator, JotaiRootDecorator], +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectColor.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectColor.stories.tsx new file mode 100644 index 0000000000..4ccd34eb40 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectColor.stories.tsx @@ -0,0 +1,85 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { type ColorSampleVariant } from '@ui/display'; +import { + CatalogDecorator, + type CatalogDimension, + type CatalogOptions, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; +import { type ThemeColor, MAIN_COLOR_NAMES } from '@ui/theme'; +import { MenuItemSelectColor } from '../MenuItemSelectColor'; + +const meta: Meta = { + title: 'UI/Navigation/Menu/MenuItem/MenuItemSelectColor', + component: MenuItemSelectColor, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { color: 'green' }, + argTypes: { className: { control: false } }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + argTypes: { className: { control: false } }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, + catalog: { + dimensions: [ + { + name: 'color', + values: MAIN_COLOR_NAMES, + props: (color: ThemeColor) => ({ color }), + labels: (color: ThemeColor) => color, + }, + { + name: 'states', + values: [ + 'default', + 'hover', + 'disabled', + 'selected', + 'hover+selected', + ], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + return { className: 'hover' }; + case 'disabled': + return { disabled: true }; + case 'selected': + return { selected: true }; + + case 'hover+selected': + return { className: 'hover', selected: true }; + default: + return {}; + } + }, + }, + { + name: 'variant', + values: ['circle', 'default', 'pipeline'], + props: (variant: ColorSampleVariant) => ({ variant }), + labels: (variant: ColorSampleVariant) => variant, + }, + ] as CatalogDimension[], + options: { + elementContainer: { + width: 200, + }, + } as CatalogOptions, + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectTag.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectTag.stories.tsx new file mode 100644 index 0000000000..dc8891703f --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectTag.stories.tsx @@ -0,0 +1,96 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { + CatalogDecorator, + type CatalogDimension, + type CatalogOptions, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; +import { type ThemeColor } from '@ui/theme'; +import { MenuItemSelectTag } from '../MenuItemSelectTag'; + +const meta: Meta = { + title: 'UI/Navigation/Menu/MenuItem/MenuItemSelectTag', + component: MenuItemSelectTag, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + selected: false, + onClick: undefined, + text: 'Item A', + }, + argTypes: { + selected: { + control: 'boolean', + defaultValue: false, + }, + onClick: { + control: false, + }, + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: { + text: 'Item A', + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { + hover: ['.hover'], + active: ['.pressed'], + focus: ['.focus'], + }, + catalog: { + dimensions: [ + { + name: 'color', + values: [ + 'green', + 'turquoise', + 'sky', + 'blue', + 'purple', + 'pink', + 'red', + 'orange', + 'yellow', + 'gray', + ], + props: (color: ThemeColor) => ({ color }), + }, + { + name: 'states', + values: ['default', 'hover', 'selected', 'hover+selected'], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + return { className: 'hover' }; + case 'selected': + return { selected: true }; + case 'hover+selected': + return { className: 'hover', selected: true }; + default: + return {}; + } + }, + labels: (state: string) => `State: ${state}`, + }, + ] as CatalogDimension[], + options: { + elementContainer: { width: 200 }, + } as CatalogOptions, + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemToggle.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemToggle.stories.tsx new file mode 100644 index 0000000000..db3a919fce --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemToggle.stories.tsx @@ -0,0 +1,89 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; + +import { IconBell } from '@ui/display'; +import { + CatalogDecorator, + type CatalogDimension, + type CatalogOptions, + type CatalogStory, + ComponentDecorator, +} from '@ui/testing'; +import { MenuItemToggle } from '../MenuItemToggle'; + +const meta: Meta = { + title: 'UI/Navigation/Menu/MenuItem/MenuItemToggle', + component: MenuItemToggle, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + parameters: { a11y: { test: 'todo' } }, + args: { + text: 'First option', + }, + decorators: [ComponentDecorator], +}; + +export const Catalog: CatalogStory = { + args: { LeftIcon: IconBell, text: 'Menu item' }, + argTypes: { + className: { control: false }, + }, + parameters: { + // TODO(a11y): violations inherited from deprecated story; fix during a11y pass + a11y: { test: 'todo' }, + pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] }, + catalog: { + dimensions: [ + { + name: 'withIcon', + values: [true, false], + props: (withIcon: boolean) => ({ + LeftIcon: withIcon ? IconBell : undefined, + }), + labels: (withIcon: boolean) => + withIcon ? 'With left icon' : 'Without left icon', + }, + { + name: 'withIconContainer', + values: [true, false], + props: (withIconContainer: boolean) => ({ withIconContainer }), + labels: (withIconContainer: boolean) => + withIconContainer + ? 'With icon container' + : 'Without icon container', + }, + { + name: 'toggled', + values: [true, false], + props: (toggled: boolean) => ({ toggled }), + labels: (toggled: boolean) => (toggled ? 'Toggled' : 'Not toggled'), + }, + { + name: 'states', + values: ['default', 'hover'], + props: (state: string) => { + switch (state) { + case 'default': + return {}; + case 'hover': + return { className: state }; + default: + return {}; + } + }, + }, + ] satisfies CatalogDimension[], + options: { + elementContainer: { + width: 200, + }, + } satisfies CatalogOptions, + }, + }, + decorators: [CatalogDecorator], +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/constants/DefaultColorLabels.ts b/packages/twenty-ui/src/navigation/menu/menu-item/constants/DefaultColorLabels.ts new file mode 100644 index 0000000000..daa8524ff0 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/constants/DefaultColorLabels.ts @@ -0,0 +1,31 @@ +import { type ThemeColor } from '@ui/theme'; + +export type ColorLabels = Record; + +export const DEFAULT_COLOR_LABELS: ColorLabels = { + gray: 'Gray', + tomato: 'Tomato', + red: 'Red', + ruby: 'Ruby', + crimson: 'Crimson', + pink: 'Pink', + plum: 'Plum', + purple: 'Purple', + violet: 'Violet', + iris: 'Iris', + cyan: 'Cyan', + turquoise: 'Turquoise', + sky: 'Sky', + blue: 'Blue', + jade: 'Jade', + green: 'Green', + grass: 'Grass', + mint: 'Mint', + lime: 'Lime', + bronze: 'Bronze', + gold: 'Gold', + brown: 'Brown', + orange: 'Orange', + amber: 'Amber', + yellow: 'Yellow', +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIcon.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIcon.tsx new file mode 100644 index 0000000000..4667780a88 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIcon.tsx @@ -0,0 +1,51 @@ +import { type IconComponent, TintedIconTile } from '@ui/display'; +import { type ThemeColor } from '@ui/theme'; +import { ThemeContext } from '@ui/theme-constants'; +import { useContext } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; +import { MenuItemIconBoxContainer } from './MenuItemIconBoxContainer'; + +export type MenuItemIconProps = { + Icon: IconComponent | null | undefined; + iconThemeColor?: ThemeColor | null; + withContainer?: boolean; + withContainerBackground?: boolean; +}; + +export const MenuItemIcon = ({ + Icon, + iconThemeColor, + withContainer = false, + withContainerBackground = true, +}: MenuItemIconProps) => { + const { theme } = useContext(ThemeContext); + + if (!Icon) { + return null; + } + + if (isDefined(iconThemeColor)) { + return ( + + ); + } + + const iconElement = ( + + ); + + if (withContainer) { + return ( + + {iconElement} + + ); + } + + return iconElement; +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconBoxContainer.module.scss b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconBoxContainer.module.scss new file mode 100644 index 0000000000..de468042d5 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconBoxContainer.module.scss @@ -0,0 +1,11 @@ +.iconContainer { + align-items: flex-start; + border-radius: var(--t-border-radius-sm); + display: flex; + flex-direction: column; + padding: var(--t-spacing-1); +} + +.withBackground { + background: var(--t-background-transparent-light); +} diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconBoxContainer.module.scss.d.ts b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconBoxContainer.module.scss.d.ts new file mode 100644 index 0000000000..43552f182f --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconBoxContainer.module.scss.d.ts @@ -0,0 +1,5 @@ +declare const classNames: { + readonly iconContainer: 'iconContainer'; + readonly withBackground: 'withBackground'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconBoxContainer.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconBoxContainer.tsx new file mode 100644 index 0000000000..b3156f9438 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconBoxContainer.tsx @@ -0,0 +1,26 @@ +import { clsx } from 'clsx'; +import { type ReactNode } from 'react'; + +import styles from './MenuItemIconBoxContainer.module.scss'; + +export const StyledIconContainer = ({ + children, + className, + hasBackground = true, +}: { + children?: ReactNode; + className?: string; + hasBackground?: boolean; +}) => ( +
    + {children} +
    +); + +export { StyledIconContainer as MenuItemIconBoxContainer }; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconWithGripSwap.module.scss b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconWithGripSwap.module.scss new file mode 100644 index 0000000000..f1fbda33ba --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconWithGripSwap.module.scss @@ -0,0 +1,18 @@ +.iconSwapContainer { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.defaultIcon { + display: flex; + transition: opacity duration(instant) ease; +} + +.hoverIcon { + position: absolute; + display: flex; + opacity: 0; + transition: opacity duration(instant) ease; +} diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconWithGripSwap.module.scss.d.ts b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconWithGripSwap.module.scss.d.ts new file mode 100644 index 0000000000..154f9d6109 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconWithGripSwap.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly iconSwapContainer: 'iconSwapContainer'; + readonly defaultIcon: 'defaultIcon'; + readonly hoverIcon: 'hoverIcon'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconWithGripSwap.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconWithGripSwap.tsx new file mode 100644 index 0000000000..d5589b2107 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemIconWithGripSwap.tsx @@ -0,0 +1,63 @@ +import { + type IconComponent, + IconGripVertical, + TintedIconTile, +} from '@ui/display'; +import { type ThemeColor } from '@ui/theme'; +import { ThemeContext } from '@ui/theme-constants'; +import { clsx } from 'clsx'; +import { useContext } from 'react'; +import { isDefined } from '@ui/utilities/utils/isDefined'; +import { MenuItemIconBoxContainer } from './MenuItemIconBoxContainer'; + +import styles from './MenuItemIconWithGripSwap.module.scss'; + +export type MenuItemIconWithGripSwapProps = { + LeftIcon: IconComponent | null | undefined; + iconThemeColor?: ThemeColor | null; + withIconContainer?: boolean; + gripIconColor: string; +}; + +export const MenuItemIconWithGripSwap = ({ + LeftIcon, + iconThemeColor, + withIconContainer = false, + gripIconColor, +}: MenuItemIconWithGripSwapProps) => { + const { theme } = useContext(ThemeContext); + + if (!LeftIcon) { + return null; + } + + const iconContent = ( +
    +
    + {isDefined(iconThemeColor) ? ( + + ) : ( + + )} +
    +
    + +
    +
    + ); + + if (withIconContainer && !isDefined(iconThemeColor)) { + return {iconContent}; + } + + return iconContent; +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemLeftContent.module.scss b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemLeftContent.module.scss new file mode 100644 index 0000000000..b3018dd774 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemLeftContent.module.scss @@ -0,0 +1,17 @@ +.mainText { + flex-shrink: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; +} + +.menuItemLabelRight { + display: flex; + flex-direction: row; + font-size: var(--t-font-size-md); + font-weight: var(--t-font-weight-regular); + overflow: hidden; + white-space: nowrap; + margin-left: auto; +} diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemLeftContent.module.scss.d.ts b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemLeftContent.module.scss.d.ts new file mode 100644 index 0000000000..7ec8d6bf73 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemLeftContent.module.scss.d.ts @@ -0,0 +1,5 @@ +declare const classNames: { + readonly mainText: 'mainText'; + readonly menuItemLabelRight: 'menuItemLabelRight'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemLeftContent.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemLeftContent.tsx new file mode 100644 index 0000000000..9830e57a76 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemLeftContent.tsx @@ -0,0 +1,134 @@ +import { isNonEmptyString, isString } from '@sniptt/guards'; +import { type ReactNode, useContext } from 'react'; + +import { + type IconComponent, + IconGripVertical, + OverflowingTextWithTooltip, +} from '@ui/display'; +import { type ThemeColor } from '@ui/theme'; +import { ThemeContext, themeCssVariables } from '@ui/theme-constants'; +import { type MenuItemDraggableGripMode } from '../../types/MenuItemDraggableGripMode'; +import { MenuItemIcon } from './MenuItemIcon'; +import { MenuItemIconBoxContainer } from './MenuItemIconBoxContainer'; +import { MenuItemIconWithGripSwap } from './MenuItemIconWithGripSwap'; +import { + StyledDraggableItem, + StyledMenuItemContextualText, + StyledMenuItemLabel, + StyledMenuItemLeftContent, + StyledRightMenuItemContextualText, +} from './StyledMenuItemBase'; + +import styles from './MenuItemLeftContent.module.scss'; + +type MenuItemLeftContentProps = { + className?: string; + LeftComponent?: ReactNode; + LeftIcon: IconComponent | null | undefined; + iconThemeColor?: ThemeColor | null; + withIconContainer?: boolean; + withIconContainerBackground?: boolean; + gripMode?: MenuItemDraggableGripMode; + disabled?: boolean; + text: ReactNode; + contextualText?: ReactNode; + contextualTextPosition?: 'left' | 'right'; +}; + +export const MenuItemLeftContent = ({ + className, + LeftComponent, + LeftIcon, + iconThemeColor, + withIconContainer = false, + withIconContainerBackground = true, + text, + contextualText, + contextualTextPosition = 'left', + gripMode = 'never', + disabled = false, +}: MenuItemLeftContentProps) => { + const { theme } = useContext(ThemeContext); + + const gripIconColor = withIconContainer + ? themeCssVariables.font.color.tertiary + : disabled + ? themeCssVariables.font.color.extraLight + : themeCssVariables.font.color.light; + + return ( + + {gripMode === 'always' && + (withIconContainer ? ( + + + + + + ) : ( + + + + ))} + {gripMode === 'onHover' ? ( + + ) : ( + + )} + {LeftComponent} + + {isString(text) ? ( +
    + +
    + ) : ( + text + )} + {contextualTextPosition === 'left' && ( + <> + {isString(contextualText) + ? isNonEmptyString(contextualText) && ( + + + + ) + : contextualText} + + )} +
    + {contextualTextPosition === 'right' && ( +
    + + {isString(contextualText) ? ( + + ) : ( + contextualText + )} + +
    + )} +
    + ); +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/StyledMenuItemBase.module.scss b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/StyledMenuItemBase.module.scss new file mode 100644 index 0000000000..82dddb2a1a --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/StyledMenuItemBase.module.scss @@ -0,0 +1,203 @@ +// Shared base replicating the deprecated Linaria StyledMenuItemBase / +// StyledHoverableMenuItemBase static rules. +@mixin menu-item-base { + --horizontal-padding: var(--t-spacing-1); + --vertical-padding: var(--t-spacing-2); + + // Height and width below are sized against the content box. + box-sizing: content-box; + align-items: center; + border-radius: var(--t-border-radius-sm); + display: flex; + flex-direction: row; + font-size: var(--t-font-size-sm); + gap: var(--t-spacing-2); + height: calc(32px - 2 * var(--vertical-padding)); + justify-content: space-between; + padding: var(--vertical-padding) var(--horizontal-padding); + position: relative; + user-select: none; + width: calc(100% - 2 * var(--horizontal-padding)); + background: transparent; + transition: background 0.1s ease; + color: var(--t-font-color-secondary); +} + +// Cascade order mirrors the deprecated Linaria ternaries: +// color: disabled > accent > base; hover background: disabled (with +// key-selected/focused exception) > danger > hover-background-disabled > base. +@mixin menu-item-base-states { + &[data-key-selected], + &[data-focused] { + background: var(--t-background-transparent-light); + } + + &[data-accent='danger'] { + color: var(--t-font-color-danger); + } + + &[data-accent='placeholder'] { + color: var(--t-font-color-tertiary); + } + + &[data-hover-background-disabled] { + transition: none; + } + + &[data-disabled] { + color: var(--t-font-color-tertiary); + transition: none; + } + + &:hover { + background: var(--t-background-transparent-light); + } + + &[data-hover-background-disabled]:hover { + background: transparent; + } + + &[data-accent='danger']:hover { + background: var(--t-background-transparent-danger); + } + + &[data-disabled]:hover { + background: transparent; + } + + &[data-disabled][data-key-selected]:hover, + &[data-disabled][data-focused]:hover { + background: var(--t-background-transparent-light); + } +} + +.menuItemBase { + @include menu-item-base; + + cursor: pointer; + + @include menu-item-base-states; +} + +.hoverableMenuItemBase { + @include menu-item-base; + + cursor: pointer; + + @include menu-item-base-states; + + &[data-cursor='drag'] { + cursor: grab; + } + + &[data-disabled] { + cursor: default; + } +} + +// `.hoverable-buttons`, `.grip-swap-default-icon` and `.grip-swap-hover-icon` +// are global hooks: consumers pass them as string classNames. +.hoverableMenuItemBase :global(.hoverable-buttons) { + opacity: 1; + right: auto; + transition: opacity duration(instant) ease; +} + +.hoverableMenuItemBase[data-icon-displayed-on-hover-only] + :global(.hoverable-buttons) { + opacity: 0; + right: var(--t-spacing-2); +} + +.hoverableMenuItemBase:hover :global(.hoverable-buttons) { + opacity: 1; +} + +.hoverableMenuItemBase:hover :global(.grip-swap-default-icon) { + opacity: 0; +} + +.hoverableMenuItemBase:hover :global(.grip-swap-hover-icon) { + opacity: 1; +} + +.menuItemLabel { + display: flex; + flex-direction: row; + font-size: var(--t-font-size-md); + font-weight: var(--t-font-weight-regular); + overflow: hidden; + white-space: nowrap; +} + +.menuItemLabelLight { + color: var(--t-font-color-light); + display: flex; + flex-direction: row; + font-size: var(--t-font-size-md); + font-weight: var(--t-font-weight-regular); + overflow: hidden; + white-space: nowrap; +} + +.noIconFiller { + width: var(--t-spacing-1); +} + +.menuItemLeftContent { + align-items: center; + display: flex; + flex-direction: row; + gap: var(--t-spacing-2); + min-width: 0; + width: 100%; +} + +.menuItemLeftContent svg { + flex-shrink: 0; +} + +.menuItemRightContent { + align-items: center; + display: flex; + flex-direction: row; + gap: var(--t-spacing-2); +} + +.menuItemRightContent svg { + flex-shrink: 0; +} + +.draggableItem { + cursor: grab; + align-items: center; + display: flex; +} + +.menuItemIconCheckContainer { + align-items: center; + display: flex; + flex-shrink: 0; + margin-right: var(--t-spacing-1); +} + +.menuItemContextualText { + color: var(--t-font-color-light); + font-family: inherit; + font-size: inherit; + font-weight: inherit; + padding-left: var(--t-spacing-1); + flex-shrink: 1; + overflow: hidden; +} + +.rightMenuItemContextualText { + color: var(--t-font-color-light); + font-family: inherit; + font-size: inherit; + font-weight: inherit; + padding-left: var(--t-spacing-1); + flex-shrink: 1; + overflow: hidden; + text-align: right; +} diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/StyledMenuItemBase.module.scss.d.ts b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/StyledMenuItemBase.module.scss.d.ts new file mode 100644 index 0000000000..32d6bcbcfe --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/StyledMenuItemBase.module.scss.d.ts @@ -0,0 +1,14 @@ +declare const classNames: { + readonly menuItemBase: 'menuItemBase'; + readonly hoverableMenuItemBase: 'hoverableMenuItemBase'; + readonly menuItemLabel: 'menuItemLabel'; + readonly menuItemLabelLight: 'menuItemLabelLight'; + readonly noIconFiller: 'noIconFiller'; + readonly menuItemLeftContent: 'menuItemLeftContent'; + readonly menuItemRightContent: 'menuItemRightContent'; + readonly draggableItem: 'draggableItem'; + readonly menuItemIconCheckContainer: 'menuItemIconCheckContainer'; + readonly menuItemContextualText: 'menuItemContextualText'; + readonly rightMenuItemContextualText: 'rightMenuItemContextualText'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/StyledMenuItemBase.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/StyledMenuItemBase.tsx new file mode 100644 index 0000000000..d81b35c234 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/StyledMenuItemBase.tsx @@ -0,0 +1,174 @@ +import { clsx } from 'clsx'; +import { type ComponentPropsWithoutRef, forwardRef } from 'react'; + +import { IconCheck } from '@ui/display'; +import { type MenuItemAccent } from '../../types/MenuItemAccent'; + +import styles from './StyledMenuItemBase.module.scss'; + +export type MenuItemBaseProps = { + accent?: MenuItemAccent; + isKeySelected?: boolean; + isHoverBackgroundDisabled?: boolean; + hovered?: boolean; + disabled?: boolean; + focused?: boolean; +}; + +// The deprecated Linaria styled components forwarded refs and arbitrary +// native props, so the ports preserve that contract. +type StyledMenuItemBaseElementProps = MenuItemBaseProps & + ComponentPropsWithoutRef<'div'>; + +export const StyledMenuItemBase = forwardRef< + HTMLDivElement, + StyledMenuItemBaseElementProps +>( + ( + { + accent, + isKeySelected, + isHoverBackgroundDisabled, + hovered: _hovered, + disabled, + focused, + className, + children, + ...rest + }, + ref, + ) => ( +
    + {children} +
    + ), +); + +StyledMenuItemBase.displayName = 'StyledMenuItemBase'; + +type StyledStaticDivProps = ComponentPropsWithoutRef<'div'>; + +const createStyledDiv = (classNameFromModule: string, displayName: string) => { + const StyledDiv = forwardRef( + ({ className, children, ...rest }, ref) => ( +
    + {children} +
    + ), + ); + + StyledDiv.displayName = displayName; + + return StyledDiv; +}; + +export const StyledMenuItemLabel = createStyledDiv( + styles.menuItemLabel, + 'StyledMenuItemLabel', +); + +export const StyledMenuItemLabelLight = createStyledDiv( + styles.menuItemLabelLight, + 'StyledMenuItemLabelLight', +); + +export const StyledNoIconFiller = createStyledDiv( + styles.noIconFiller, + 'StyledNoIconFiller', +); + +export const StyledMenuItemLeftContent = createStyledDiv( + styles.menuItemLeftContent, + 'StyledMenuItemLeftContent', +); + +export const StyledMenuItemRightContent = createStyledDiv( + styles.menuItemRightContent, + 'StyledMenuItemRightContent', +); + +export const StyledDraggableItem = createStyledDiv( + styles.draggableItem, + 'StyledDraggableItem', +); + +type HoverableMenuItemBaseProps = { + isIconDisplayedOnHoverOnly?: boolean; + cursor?: 'drag' | 'default'; +} & MenuItemBaseProps; + +type StyledHoverableMenuItemBaseElementProps = HoverableMenuItemBaseProps & + ComponentPropsWithoutRef<'div'>; + +export const StyledHoverableMenuItemBase = forwardRef< + HTMLDivElement, + StyledHoverableMenuItemBaseElementProps +>( + ( + { + accent, + isKeySelected, + isHoverBackgroundDisabled, + hovered: _hovered, + disabled, + focused, + isIconDisplayedOnHoverOnly, + cursor, + className, + children, + ...rest + }, + ref, + ) => ( +
    + {children} +
    + ), +); + +StyledHoverableMenuItemBase.displayName = 'StyledHoverableMenuItemBase'; + +export const StyledMenuItemIconCheck = ({ size }: { size?: number }) => ( +
    + +
    +); + +export const StyledMenuItemContextualText = createStyledDiv( + styles.menuItemContextualText, + 'StyledMenuItemContextualText', +); + +export const StyledRightMenuItemContextualText = createStyledDiv( + styles.rightMenuItemContextualText, + 'StyledRightMenuItemContextualText', +); diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/types/MenuItemAccent.ts b/packages/twenty-ui/src/navigation/menu/menu-item/types/MenuItemAccent.ts new file mode 100644 index 0000000000..c67b7f0472 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/types/MenuItemAccent.ts @@ -0,0 +1 @@ +export type MenuItemAccent = 'default' | 'danger' | 'placeholder'; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/types/MenuItemDraggableGripMode.ts b/packages/twenty-ui/src/navigation/menu/menu-item/types/MenuItemDraggableGripMode.ts new file mode 100644 index 0000000000..11b5df8a09 --- /dev/null +++ b/packages/twenty-ui/src/navigation/menu/menu-item/types/MenuItemDraggableGripMode.ts @@ -0,0 +1 @@ +export type MenuItemDraggableGripMode = 'always' | 'onHover' | 'never'; diff --git a/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBar.module.scss b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBar.module.scss new file mode 100644 index 0000000000..9a49c014f9 --- /dev/null +++ b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBar.module.scss @@ -0,0 +1,7 @@ +.container { + display: flex; + gap: var(--t-spacing-4); + justify-content: center; + padding: var(--t-spacing-3); + z-index: 1001; +} diff --git a/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBar.module.scss.d.ts b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBar.module.scss.d.ts new file mode 100644 index 0000000000..8df71b4dea --- /dev/null +++ b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBar.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly container: 'container'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBar.tsx b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBar.tsx new file mode 100644 index 0000000000..b30dccfe13 --- /dev/null +++ b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBar.tsx @@ -0,0 +1,28 @@ +import { type IconComponent } from '@ui/display/icon/types/IconComponent'; + +import { NavigationBarItem } from './NavigationBarItem'; + +import styles from './NavigationBar.module.scss'; + +type NavigationBarProps = { + activeItemName: string; + items: { name: string; Icon: IconComponent; onClick: () => void }[]; +}; + +export const NavigationBar = ({ + activeItemName, + items, +}: NavigationBarProps) => { + return ( +
    + {items.map(({ Icon, name, onClick }) => ( + + ))} +
    + ); +}; diff --git a/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBarItem.module.scss b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBarItem.module.scss new file mode 100644 index 0000000000..86af7e1dfa --- /dev/null +++ b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBarItem.module.scss @@ -0,0 +1,18 @@ +.iconButton { + align-items: center; + border-radius: var(--t-spacing-1); + cursor: pointer; + display: flex; + height: var(--t-spacing-10); + justify-content: center; + transition: background-color duration(fast) ease; + width: var(--t-spacing-10); + + &[data-active] { + background-color: var(--t-background-transparent-light); + } + + &:hover { + background-color: var(--t-background-transparent-light); + } +} diff --git a/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBarItem.module.scss.d.ts b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBarItem.module.scss.d.ts new file mode 100644 index 0000000000..a106f42ab8 --- /dev/null +++ b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBarItem.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly iconButton: 'iconButton'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBarItem.tsx b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBarItem.tsx new file mode 100644 index 0000000000..86060f803c --- /dev/null +++ b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBarItem.tsx @@ -0,0 +1,33 @@ +import { useContext } from 'react'; + +import { type IconComponent } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext, themeCssVariables } from '@ui/theme-constants'; + +import styles from './NavigationBarItem.module.scss'; + +type NavigationBarItemProps = { + Icon: IconComponent; + isActive: boolean; + onClick: () => void; +}; + +export const NavigationBarItem = ({ + Icon, + isActive, + onClick, +}: NavigationBarItemProps) => { + const { theme } = useContext(ThemeContext); + + return ( +
    + +
    + ); +}; diff --git a/packages/twenty-ui/src/navigation/navigation-bar/components/__stories__/NavigationBar.stories.tsx b/packages/twenty-ui/src/navigation/navigation-bar/components/__stories__/NavigationBar.stories.tsx new file mode 100644 index 0000000000..6da40879b6 --- /dev/null +++ b/packages/twenty-ui/src/navigation/navigation-bar/components/__stories__/NavigationBar.stories.tsx @@ -0,0 +1,25 @@ +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { IconHome, IconSearch, IconSettings } from '@ui/display'; +import { ComponentDecorator } from '@ui/testing'; + +import { NavigationBar } from '../NavigationBar'; + +const meta: Meta = { + title: 'UI/Navigation/NavigationBar/NavigationBar', + component: NavigationBar, + decorators: [ComponentDecorator], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + activeItemName: 'Home', + items: [ + { name: 'Home', Icon: IconHome, onClick: () => {} }, + { name: 'Search', Icon: IconSearch, onClick: () => {} }, + { name: 'Settings', Icon: IconSettings, onClick: () => {} }, + ], + }, +}; diff --git a/packages/twenty-ui/src/navigation/notification-counter/components/NotificationCounter.module.scss b/packages/twenty-ui/src/navigation/notification-counter/components/NotificationCounter.module.scss new file mode 100644 index 0000000000..9c27da0b8f --- /dev/null +++ b/packages/twenty-ui/src/navigation/notification-counter/components/NotificationCounter.module.scss @@ -0,0 +1,20 @@ +.notificationCounter { + width: 14px; + height: 14px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: var(--t-font-size-xxs); + font-weight: var(--t-font-weight-semi-bold); +} + +.primary { + background: var(--t-color-blue); + color: white; +} + +.secondary { + background: var(--t-background-transparent-light); + color: var(--t-font-color-secondary); +} diff --git a/packages/twenty-ui/src/navigation/notification-counter/components/NotificationCounter.module.scss.d.ts b/packages/twenty-ui/src/navigation/notification-counter/components/NotificationCounter.module.scss.d.ts new file mode 100644 index 0000000000..126e4c2577 --- /dev/null +++ b/packages/twenty-ui/src/navigation/notification-counter/components/NotificationCounter.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly notificationCounter: 'notificationCounter'; + readonly primary: 'primary'; + readonly secondary: 'secondary'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/navigation/notification-counter/components/NotificationCounter.tsx b/packages/twenty-ui/src/navigation/notification-counter/components/NotificationCounter.tsx new file mode 100644 index 0000000000..17d9ff2abf --- /dev/null +++ b/packages/twenty-ui/src/navigation/notification-counter/components/NotificationCounter.tsx @@ -0,0 +1,23 @@ +import { clsx } from 'clsx'; + +import styles from './NotificationCounter.module.scss'; + +type NotificationCounterProps = { + count: number; + variant?: 'primary' | 'secondary'; + className?: string; +}; + +export const NotificationCounter = ({ + count, + variant = 'primary', + className, +}: NotificationCounterProps) => { + return ( +
    + {count} +
    + ); +}; diff --git a/packages/twenty-ui/src/testing/ComponentStorybookLayout.module.scss b/packages/twenty-ui/src/testing/ComponentStorybookLayout.module.scss new file mode 100644 index 0000000000..1beab499c4 --- /dev/null +++ b/packages/twenty-ui/src/testing/ComponentStorybookLayout.module.scss @@ -0,0 +1,11 @@ +.layout { + background: var(--t-background-primary); + border: 1px solid var(--t-border-color-light); + border-radius: 5px; + + display: flex; + flex-direction: row; + + max-width: calc(100% - 40px); + padding: 20px; +} diff --git a/packages/twenty-ui/src/testing/ComponentStorybookLayout.module.scss.d.ts b/packages/twenty-ui/src/testing/ComponentStorybookLayout.module.scss.d.ts new file mode 100644 index 0000000000..a5c9014be6 --- /dev/null +++ b/packages/twenty-ui/src/testing/ComponentStorybookLayout.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly layout: 'layout'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/testing/ComponentStorybookLayout.tsx b/packages/twenty-ui/src/testing/ComponentStorybookLayout.tsx new file mode 100644 index 0000000000..f932cf4396 --- /dev/null +++ b/packages/twenty-ui/src/testing/ComponentStorybookLayout.tsx @@ -0,0 +1,32 @@ +import { isDefined } from '@ui/utilities/utils/isDefined'; + +import styles from './ComponentStorybookLayout.module.scss'; + +type ComponentStorybookLayoutProps = { + width?: number; + backgroundColor?: string | undefined; + height?: number; + children: JSX.Element; +}; + +export const ComponentStorybookLayout = ({ + width, + backgroundColor, + height, + children, +}: ComponentStorybookLayoutProps) => { + return ( +
    + {children} +
    + ); +}; diff --git a/packages/twenty-ui/src/testing/decorators/CatalogDecorator.module.scss b/packages/twenty-ui/src/testing/decorators/CatalogDecorator.module.scss new file mode 100644 index 0000000000..5b705a52c0 --- /dev/null +++ b/packages/twenty-ui/src/testing/decorators/CatalogDecorator.module.scss @@ -0,0 +1,65 @@ +.columnTitle { + font-size: var(--t-font-size-lg); + font-weight: var(--t-font-weight-semi-bold); + margin: var(--t-spacing-2); +} + +.rowsTitle { + color: var(--t-font-color-secondary); + font-size: var(--t-font-size-md); + font-weight: var(--t-font-weight-semi-bold); + margin: var(--t-spacing-2); + width: 100px; +} + +.rowTitle { + color: var(--t-font-color-tertiary); + font-size: var(--t-font-size-md); + font-weight: var(--t-font-weight-semi-bold); + margin: var(--t-spacing-2); + width: 100px; +} + +.elementTitle { + color: var(--t-font-color-light); + font-size: var(--t-font-size-xs); + font-weight: var(--t-font-weight-semi-bold); + margin-bottom: var(--t-spacing-1); + text-align: center; + text-transform: uppercase; +} + +.container { + display: flex; + flex-direction: row; +} + +.columnContainer { + display: flex; + flex-direction: column; + padding: var(--t-spacing-2); +} + +.rowsContainer { + display: flex; + flex-direction: column; + gap: var(--t-spacing-2); +} + +.rowContainer { + display: flex; + flex: 1; + flex-direction: row; + gap: var(--t-spacing-2); +} + +.elementContainer { + display: flex; +} + +.cellContainer { + align-items: center; + display: flex; + flex-direction: column; + padding: var(--t-spacing-2); +} diff --git a/packages/twenty-ui/src/testing/decorators/CatalogDecorator.module.scss.d.ts b/packages/twenty-ui/src/testing/decorators/CatalogDecorator.module.scss.d.ts new file mode 100644 index 0000000000..dc96cff086 --- /dev/null +++ b/packages/twenty-ui/src/testing/decorators/CatalogDecorator.module.scss.d.ts @@ -0,0 +1,13 @@ +declare const classNames: { + readonly columnTitle: 'columnTitle'; + readonly rowsTitle: 'rowsTitle'; + readonly rowTitle: 'rowTitle'; + readonly elementTitle: 'elementTitle'; + readonly container: 'container'; + readonly columnContainer: 'columnContainer'; + readonly rowsContainer: 'rowsContainer'; + readonly rowContainer: 'rowContainer'; + readonly elementContainer: 'elementContainer'; + readonly cellContainer: 'cellContainer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/testing/decorators/CatalogDecorator.tsx b/packages/twenty-ui/src/testing/decorators/CatalogDecorator.tsx new file mode 100644 index 0000000000..88d8dc309e --- /dev/null +++ b/packages/twenty-ui/src/testing/decorators/CatalogDecorator.tsx @@ -0,0 +1,117 @@ +import { isNumber, isString } from '@sniptt/guards'; +import { type Decorator } from '@storybook/react-vite'; +import { clsx } from 'clsx'; +import { type ComponentProps, type JSX } from 'react'; + +import styles from './CatalogDecorator.module.scss'; + +const emptyDimension = { + name: '', + values: [undefined], + props: () => ({}), +} as CatalogDimension; + +const isStringOrNumber = (term: unknown): term is string | number => + isString(term) || isNumber(term); + +export type CatalogDimension< + ComponentType extends React.ElementType = () => JSX.Element, +> = { + name: string; + values: any[]; + props: (value: any) => Partial>; + labels?: (value: any) => string; +}; + +export type CatalogOptions = { + elementContainer?: { + width?: number; + style?: React.CSSProperties; + className?: string; + }; +}; + +export const CatalogDecorator: Decorator = (Story, context) => { + const { + catalog: { dimensions = [], options = {} } = { + dimensions: [], + options: {}, + }, + } = context.parameters || {}; + + if (!dimensions || !Array.isArray(dimensions)) { + return ; + } + + const [ + dimension1 = emptyDimension, + dimension2 = emptyDimension, + dimension3 = emptyDimension, + dimension4 = emptyDimension, + ] = dimensions as CatalogDimension[]; + + return ( +
    + {dimension4.values.map((value4: any, index4: number) => ( +
    +

    + {dimension4.labels?.(value4) ?? + (isStringOrNumber(value4) ? value4 : '')} +

    + {dimension3.values.map((value3: any, index3: number) => ( +
    +

    + {dimension3.labels?.(value3) ?? + (isStringOrNumber(value3) ? value3 : '')} +

    + {dimension2.values.map((value2: any, index2: number) => ( +
    +

    + {dimension2.labels?.(value2) ?? + (isStringOrNumber(value2) ? value2 : '')} +

    + {dimension1.values.map((value1: any, index1: number) => { + return ( +
    + + {dimension1.labels?.(value1) ?? + (isStringOrNumber(value1) ? value1 : '')} + +
    + +
    +
    + ); + })} +
    + ))} +
    + ))} +
    + ))} +
    + ); +}; diff --git a/packages/twenty-ui/src/testing/decorators/ComponentDecorator.tsx b/packages/twenty-ui/src/testing/decorators/ComponentDecorator.tsx new file mode 100644 index 0000000000..6e24cd0d54 --- /dev/null +++ b/packages/twenty-ui/src/testing/decorators/ComponentDecorator.tsx @@ -0,0 +1,40 @@ +import { type Decorator } from '@storybook/react-vite'; + +import { themeCssVariables } from '@ui/theme-constants'; + +import { ComponentStorybookLayout } from '../ComponentStorybookLayout'; + +const getBackgroundColor = ( + inverted: boolean, + accent: string, +): string | undefined => { + if (!inverted) return undefined; + + switch (accent) { + case 'default': + return themeCssVariables.grayScale.gray11; + case 'danger': + return themeCssVariables.color.red; + case 'blue': + return themeCssVariables.color.blue; + default: + return undefined; + } +}; + +export const ComponentDecorator: Decorator = (Story, context) => { + const { container } = context.parameters; + const inverted = context.args.inverted as boolean; + const accent = context.args.accent as string; + const backgroundColor = getBackgroundColor(inverted, accent); + + return ( + + + + ); +}; diff --git a/packages/twenty-ui/src/testing/decorators/ComponentWithRouterDecorator.tsx b/packages/twenty-ui/src/testing/decorators/ComponentWithRouterDecorator.tsx new file mode 100644 index 0000000000..ed0044fc84 --- /dev/null +++ b/packages/twenty-ui/src/testing/decorators/ComponentWithRouterDecorator.tsx @@ -0,0 +1,82 @@ +import { type Decorator } from '@storybook/react-vite'; +import { + createMemoryRouter, + createRoutesFromElements, + Outlet, + Route, + RouterProvider, +} from 'react-router-dom'; + +import { ComponentStorybookLayout } from '../ComponentStorybookLayout'; + +interface StrictArgs { + [name: string]: unknown; +} +export type RouteParams = { + [param: string]: string; +}; + +export const isRouteParams = (obj: any): obj is RouteParams => { + if (typeof obj !== 'object' || obj === null) { + return false; + } + + return Object.keys(obj).every((key) => typeof obj[key] === 'string'); +}; + +export const computeLocation = ( + routePath: string, + routeParams?: RouteParams, +) => { + return { + pathname: routePath.replace( + /:(\w+)/g, + (paramName) => routeParams?.[paramName] ?? '', + ), + }; +}; + +const Providers = () => ( + + + +); + +const createRouter = ({ + Story, + args, + initialEntries, + initialIndex, +}: { + Story: () => JSX.Element; + args: StrictArgs; + initialEntries?: { + pathname: string; + }[]; + initialIndex?: number; +}) => + createMemoryRouter( + createRoutesFromElements( + }> + } /> + , + ), + { initialEntries, initialIndex }, + ); + +export const ComponentWithRouterDecorator: Decorator = (Story, { args }) => { + return ( + + ); +}; diff --git a/packages/twenty-ui/src/testing/decorators/JotaiRootDecorator.tsx b/packages/twenty-ui/src/testing/decorators/JotaiRootDecorator.tsx new file mode 100644 index 0000000000..b4ec98bce4 --- /dev/null +++ b/packages/twenty-ui/src/testing/decorators/JotaiRootDecorator.tsx @@ -0,0 +1,9 @@ +import { type Decorator } from '@storybook/react-vite'; + +import { Provider as JotaiProvider } from 'jotai'; + +export const JotaiRootDecorator: Decorator = (Story) => ( + + + +); diff --git a/packages/twenty-ui/src/testing/decorators/RouterDecorator.tsx b/packages/twenty-ui/src/testing/decorators/RouterDecorator.tsx new file mode 100644 index 0000000000..7f1be7046f --- /dev/null +++ b/packages/twenty-ui/src/testing/decorators/RouterDecorator.tsx @@ -0,0 +1,8 @@ +import { type Decorator } from '@storybook/react-vite'; +import { MemoryRouter } from 'react-router-dom'; + +export const RouterDecorator: Decorator = (Story) => ( + + + +); diff --git a/packages/twenty-ui/src/testing/index.ts b/packages/twenty-ui/src/testing/index.ts index a3046804c0..8d8675d81b 100644 --- a/packages/twenty-ui/src/testing/index.ts +++ b/packages/twenty-ui/src/testing/index.ts @@ -7,4 +7,20 @@ * |___/ */ -export {}; +export { ComponentStorybookLayout } from './ComponentStorybookLayout'; +export type { + CatalogDimension, + CatalogOptions, +} from './decorators/CatalogDecorator'; +export { CatalogDecorator } from './decorators/CatalogDecorator'; +export { ComponentDecorator } from './decorators/ComponentDecorator'; +export type { RouteParams } from './decorators/ComponentWithRouterDecorator'; +export { + isRouteParams, + computeLocation, + ComponentWithRouterDecorator, +} from './decorators/ComponentWithRouterDecorator'; +export { JotaiRootDecorator } from './decorators/JotaiRootDecorator'; +export { RouterDecorator } from './decorators/RouterDecorator'; +export { AVATAR_URL_MOCK } from './mocks/avatarUrlMock'; +export type { CatalogStory } from './types/CatalogStory'; diff --git a/packages/twenty-ui/src/testing/mocks/avatarUrlMock.ts b/packages/twenty-ui/src/testing/mocks/avatarUrlMock.ts new file mode 100644 index 0000000000..c8b2cc3d2b --- /dev/null +++ b/packages/twenty-ui/src/testing/mocks/avatarUrlMock.ts @@ -0,0 +1,2 @@ +export const AVATAR_URL_MOCK = + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAYABgAAD/4QCMRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABgAAAAAQAAAGAAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAABSgAwAEAAAAAQAAABQAAAAA/8AAEQgAFAAUAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMACwgICggHCwoJCg0MCw0RHBIRDw8RIhkaFBwpJCsqKCQnJy0yQDctMD0wJyc4TDk9Q0VISUgrNk9VTkZUQEdIRf/bAEMBDA0NEQ8RIRISIUUuJy5FRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRf/dAAQAAv/aAAwDAQACEQMRAD8Ava1q728otYY98joSCTgZrnbXWdTtrhrfVZXWLafmcAEkdgR/hVltQku9Q8+OIEBcGOT+ID0PY1ka1KH2u8ToqnPLbmIqG7u6LtbQ7RXBRec4Uck9eKXcPWsKDWVnhWSL5kYcFelSf2m3901POh8jP//QoyIAnTuKpXsY82NsksUyWPU5q/L9z8RVK++/F/uCsVsaEURwgA4HtT9x9TUcf3KfUGh//9k='; diff --git a/packages/twenty-ui/src/testing/types/CatalogStory.ts b/packages/twenty-ui/src/testing/types/CatalogStory.ts new file mode 100644 index 0000000000..caf2da26f1 --- /dev/null +++ b/packages/twenty-ui/src/testing/types/CatalogStory.ts @@ -0,0 +1,24 @@ +import { type StoryObj } from '@storybook/react-vite'; +import { type ElementType } from 'react'; + +import { + type CatalogDimension, + type CatalogOptions, +} from '../decorators/CatalogDecorator'; + +export type CatalogStory< + StoryType extends StoryObj, + ComponentType extends ElementType, +> = { + args?: StoryType['args']; + argTypes?: StoryType['argTypes']; + play?: StoryType['play']; + render?: StoryType['render']; + parameters: StoryType['parameters'] & { + catalog: { + dimensions: CatalogDimension[]; + options?: CatalogOptions; + }; + }; + decorators: StoryType['decorators']; +}; diff --git a/packages/twenty-ui/src/theme-constants/__tests__/getNextThemeColor.test.ts b/packages/twenty-ui/src/theme-constants/__tests__/getNextThemeColor.test.ts index d1e316dccb..b8b126433b 100644 --- a/packages/twenty-ui/src/theme-constants/__tests__/getNextThemeColor.test.ts +++ b/packages/twenty-ui/src/theme-constants/__tests__/getNextThemeColor.test.ts @@ -1,7 +1,7 @@ import { MAIN_COLOR_NAMES, type ThemeColor, -} from '@new-ui/theme/constants/MainColorNames'; +} from '@ui/theme/constants/MainColorNames'; import { getNextThemeColor } from '../getNextThemeColor'; diff --git a/packages/twenty-ui/src/theme-constants/getNextThemeColor.ts b/packages/twenty-ui/src/theme-constants/getNextThemeColor.ts index ce8dfdc8e0..7cdcc7ba9a 100644 --- a/packages/twenty-ui/src/theme-constants/getNextThemeColor.ts +++ b/packages/twenty-ui/src/theme-constants/getNextThemeColor.ts @@ -1,4 +1,4 @@ -import { type ThemeColor } from '@new-ui/theme/constants/MainColorNames'; +import { type ThemeColor } from '@ui/theme/constants/MainColorNames'; export const getNextThemeColor = ( colorNames: ThemeColor[], diff --git a/packages/twenty-ui/src/theme/constants/AccentDark.ts b/packages/twenty-ui/src/theme/constants/AccentDark.ts index c95cd81d31..a8fad2b9e9 100644 --- a/packages/twenty-ui/src/theme/constants/AccentDark.ts +++ b/packages/twenty-ui/src/theme/constants/AccentDark.ts @@ -1,5 +1,5 @@ import * as RadixColors from '@radix-ui/colors'; -import { COLOR_DARK } from '@new-ui/theme/constants/ColorsDark'; +import { COLOR_DARK } from '@ui/theme/constants/ColorsDark'; export const ACCENT_DARK = { primary: COLOR_DARK.blue5, diff --git a/packages/twenty-ui/src/theme/constants/AccentLight.ts b/packages/twenty-ui/src/theme/constants/AccentLight.ts index f8e6c7cf3c..5895d44acd 100644 --- a/packages/twenty-ui/src/theme/constants/AccentLight.ts +++ b/packages/twenty-ui/src/theme/constants/AccentLight.ts @@ -1,5 +1,5 @@ import * as RadixColors from '@radix-ui/colors'; -import { COLOR_LIGHT } from '@new-ui/theme/constants/ColorsLight'; +import { COLOR_LIGHT } from '@ui/theme/constants/ColorsLight'; export const ACCENT_LIGHT = { primary: COLOR_LIGHT.blue5, diff --git a/packages/twenty-ui/src/theme/constants/BackgroundDark.ts b/packages/twenty-ui/src/theme/constants/BackgroundDark.ts index e3d7420b6b..2d6c207d56 100644 --- a/packages/twenty-ui/src/theme/constants/BackgroundDark.ts +++ b/packages/twenty-ui/src/theme/constants/BackgroundDark.ts @@ -1,6 +1,6 @@ import * as RadixColors from '@radix-ui/colors'; -import { COLOR_DARK } from '@new-ui/theme/constants/ColorsDark'; +import { COLOR_DARK } from '@ui/theme/constants/ColorsDark'; import { GRAY_SCALE_DARK } from './GrayScaleDark'; import { TRANSPARENT_COLORS_DARK } from './TransparentColorsDark'; diff --git a/packages/twenty-ui/src/theme/constants/BackgroundLight.ts b/packages/twenty-ui/src/theme/constants/BackgroundLight.ts index 4ba0183af2..adc5bc1795 100644 --- a/packages/twenty-ui/src/theme/constants/BackgroundLight.ts +++ b/packages/twenty-ui/src/theme/constants/BackgroundLight.ts @@ -1,6 +1,6 @@ import * as RadixColors from '@radix-ui/colors'; -import { COLOR_LIGHT } from '@new-ui/theme/constants/ColorsLight'; +import { COLOR_LIGHT } from '@ui/theme/constants/ColorsLight'; import { GRAY_SCALE_LIGHT } from './GrayScaleLight'; import { TRANSPARENT_COLORS_LIGHT } from './TransparentColorsLight'; diff --git a/packages/twenty-ui/src/theme/constants/BorderDark.ts b/packages/twenty-ui/src/theme/constants/BorderDark.ts index 4105fe4407..2ae9260a56 100644 --- a/packages/twenty-ui/src/theme/constants/BorderDark.ts +++ b/packages/twenty-ui/src/theme/constants/BorderDark.ts @@ -1,4 +1,4 @@ -import { COLOR_DARK } from '@new-ui/theme/constants/ColorsDark'; +import { COLOR_DARK } from '@ui/theme/constants/ColorsDark'; import { BORDER_COMMON } from './BorderCommon'; import { GRAY_SCALE_DARK } from './GrayScaleDark'; import { TRANSPARENT_COLORS_DARK } from './TransparentColorsDark'; diff --git a/packages/twenty-ui/src/theme/constants/BorderLight.ts b/packages/twenty-ui/src/theme/constants/BorderLight.ts index 9470a40664..a104188b80 100644 --- a/packages/twenty-ui/src/theme/constants/BorderLight.ts +++ b/packages/twenty-ui/src/theme/constants/BorderLight.ts @@ -1,4 +1,4 @@ -import { COLOR_LIGHT } from '@new-ui/theme/constants/ColorsLight'; +import { COLOR_LIGHT } from '@ui/theme/constants/ColorsLight'; import { BORDER_COMMON } from './BorderCommon'; import { GRAY_SCALE_LIGHT } from './GrayScaleLight'; import { TRANSPARENT_COLORS_LIGHT } from './TransparentColorsLight'; diff --git a/packages/twenty-ui/src/theme/constants/BoxShadowDark.ts b/packages/twenty-ui/src/theme/constants/BoxShadowDark.ts index aad5ca2719..855a65e45a 100644 --- a/packages/twenty-ui/src/theme/constants/BoxShadowDark.ts +++ b/packages/twenty-ui/src/theme/constants/BoxShadowDark.ts @@ -1,4 +1,4 @@ -import { RGBA } from '@new-ui/theme/constants/Rgba'; +import { RGBA } from '@ui/theme/constants/Rgba'; /* oxlint-disable twenty/no-hardcoded-colors */ export const BOX_SHADOW_DARK = { diff --git a/packages/twenty-ui/src/theme/constants/BoxShadowLight.ts b/packages/twenty-ui/src/theme/constants/BoxShadowLight.ts index 5ac33ae303..bca674ee7e 100644 --- a/packages/twenty-ui/src/theme/constants/BoxShadowLight.ts +++ b/packages/twenty-ui/src/theme/constants/BoxShadowLight.ts @@ -1,4 +1,4 @@ -import { GRAY_SCALE_LIGHT_ALPHA } from '@new-ui/theme/constants/GrayScaleLightAlpha'; +import { GRAY_SCALE_LIGHT_ALPHA } from '@ui/theme/constants/GrayScaleLightAlpha'; export const BOX_SHADOW_LIGHT = { color: GRAY_SCALE_LIGHT_ALPHA.gray2, diff --git a/packages/twenty-ui/src/theme/constants/CodeDark.ts b/packages/twenty-ui/src/theme/constants/CodeDark.ts index afda65c33c..2cefda59d2 100644 --- a/packages/twenty-ui/src/theme/constants/CodeDark.ts +++ b/packages/twenty-ui/src/theme/constants/CodeDark.ts @@ -1,4 +1,4 @@ -import { COLOR_DARK } from '@new-ui/theme/constants/ColorsDark'; +import { COLOR_DARK } from '@ui/theme/constants/ColorsDark'; export const CODE_DARK = { text: { diff --git a/packages/twenty-ui/src/theme/constants/CodeLight.ts b/packages/twenty-ui/src/theme/constants/CodeLight.ts index 116d7ef87f..b598b08137 100644 --- a/packages/twenty-ui/src/theme/constants/CodeLight.ts +++ b/packages/twenty-ui/src/theme/constants/CodeLight.ts @@ -1,4 +1,4 @@ -import { COLOR_LIGHT } from '@new-ui/theme/constants/ColorsLight'; +import { COLOR_LIGHT } from '@ui/theme/constants/ColorsLight'; export const CODE_LIGHT = { text: { diff --git a/packages/twenty-ui/src/theme/constants/FontDark.ts b/packages/twenty-ui/src/theme/constants/FontDark.ts index e18570b5c4..3d1fada0aa 100644 --- a/packages/twenty-ui/src/theme/constants/FontDark.ts +++ b/packages/twenty-ui/src/theme/constants/FontDark.ts @@ -1,4 +1,4 @@ -import { COLOR_DARK } from '@new-ui/theme/constants/ColorsDark'; +import { COLOR_DARK } from '@ui/theme/constants/ColorsDark'; import { FONT_COMMON } from './FontCommon'; import { GRAY_SCALE_DARK } from './GrayScaleDark'; diff --git a/packages/twenty-ui/src/theme/constants/FontLight.ts b/packages/twenty-ui/src/theme/constants/FontLight.ts index ec00f79108..2870a62851 100644 --- a/packages/twenty-ui/src/theme/constants/FontLight.ts +++ b/packages/twenty-ui/src/theme/constants/FontLight.ts @@ -1,4 +1,4 @@ -import { COLOR_LIGHT } from '@new-ui/theme/constants/ColorsLight'; +import { COLOR_LIGHT } from '@ui/theme/constants/ColorsLight'; import { FONT_COMMON } from './FontCommon'; import { GRAY_SCALE_LIGHT } from './GrayScaleLight'; diff --git a/packages/twenty-ui/src/theme/constants/IllustrationIconDark.ts b/packages/twenty-ui/src/theme/constants/IllustrationIconDark.ts index 9d4ba4f561..e70f4ce417 100644 --- a/packages/twenty-ui/src/theme/constants/IllustrationIconDark.ts +++ b/packages/twenty-ui/src/theme/constants/IllustrationIconDark.ts @@ -1,5 +1,5 @@ -import { COLOR_DARK } from '@new-ui/theme/constants/ColorsDark'; -import { GRAY_SCALE_DARK } from '@new-ui/theme/constants/GrayScaleDark'; +import { COLOR_DARK } from '@ui/theme/constants/ColorsDark'; +import { GRAY_SCALE_DARK } from '@ui/theme/constants/GrayScaleDark'; export const ILLUSTRATION_ICON_DARK = { color: { diff --git a/packages/twenty-ui/src/theme/constants/IllustrationIconLight.ts b/packages/twenty-ui/src/theme/constants/IllustrationIconLight.ts index 501f8f318b..3f61bbebd8 100644 --- a/packages/twenty-ui/src/theme/constants/IllustrationIconLight.ts +++ b/packages/twenty-ui/src/theme/constants/IllustrationIconLight.ts @@ -1,5 +1,5 @@ -import { COLOR_LIGHT } from '@new-ui/theme/constants/ColorsLight'; -import { GRAY_SCALE_LIGHT } from '@new-ui/theme/constants/GrayScaleLight'; +import { COLOR_LIGHT } from '@ui/theme/constants/ColorsLight'; +import { GRAY_SCALE_LIGHT } from '@ui/theme/constants/GrayScaleLight'; export const ILLUSTRATION_ICON_LIGHT = { color: { diff --git a/packages/twenty-ui/src/theme/constants/MainColorsDark.ts b/packages/twenty-ui/src/theme/constants/MainColorsDark.ts index eb7e6bb987..cd76aba2a4 100644 --- a/packages/twenty-ui/src/theme/constants/MainColorsDark.ts +++ b/packages/twenty-ui/src/theme/constants/MainColorsDark.ts @@ -1,5 +1,5 @@ import * as RadixColors from '@radix-ui/colors'; -import { GRAY_SCALE_DARK } from '@new-ui/theme/constants/GrayScaleDark'; +import { GRAY_SCALE_DARK } from '@ui/theme/constants/GrayScaleDark'; export const MAIN_COLORS_DARK = { // Reds diff --git a/packages/twenty-ui/src/theme/constants/SecondaryColorsDark.ts b/packages/twenty-ui/src/theme/constants/SecondaryColorsDark.ts index 1792dc9290..01e7d11691 100644 --- a/packages/twenty-ui/src/theme/constants/SecondaryColorsDark.ts +++ b/packages/twenty-ui/src/theme/constants/SecondaryColorsDark.ts @@ -1,5 +1,5 @@ import * as RadixColors from '@radix-ui/colors'; -import { GRAY_SCALE_DARK } from '@new-ui/theme/constants/GrayScaleDark'; +import { GRAY_SCALE_DARK } from '@ui/theme/constants/GrayScaleDark'; export const SECONDARY_COLORS_DARK = { yellow1: RadixColors.yellowDarkP3.yellow1, diff --git a/packages/twenty-ui/src/theme/constants/SnackBarDark.ts b/packages/twenty-ui/src/theme/constants/SnackBarDark.ts index 981a2cb45a..3404932b7b 100644 --- a/packages/twenty-ui/src/theme/constants/SnackBarDark.ts +++ b/packages/twenty-ui/src/theme/constants/SnackBarDark.ts @@ -1,6 +1,6 @@ -import { BACKGROUND_DARK } from '@new-ui/theme/constants/BackgroundDark'; -import { FONT_DARK } from '@new-ui/theme/constants/FontDark'; -import { MAIN_COLORS_DARK } from '@new-ui/theme/constants/MainColorsDark'; +import { BACKGROUND_DARK } from '@ui/theme/constants/BackgroundDark'; +import { FONT_DARK } from '@ui/theme/constants/FontDark'; +import { MAIN_COLORS_DARK } from '@ui/theme/constants/MainColorsDark'; export const SNACK_BAR_DARK = { success: { diff --git a/packages/twenty-ui/src/theme/constants/SnackBarLight.ts b/packages/twenty-ui/src/theme/constants/SnackBarLight.ts index 1e669e4f6f..38a13940a3 100644 --- a/packages/twenty-ui/src/theme/constants/SnackBarLight.ts +++ b/packages/twenty-ui/src/theme/constants/SnackBarLight.ts @@ -1,6 +1,6 @@ -import { BACKGROUND_LIGHT } from '@new-ui/theme/constants/BackgroundLight'; -import { FONT_LIGHT } from '@new-ui/theme/constants/FontLight'; -import { MAIN_COLORS_LIGHT } from '@new-ui/theme/constants/MainColorsLight'; +import { BACKGROUND_LIGHT } from '@ui/theme/constants/BackgroundLight'; +import { FONT_LIGHT } from '@ui/theme/constants/FontLight'; +import { MAIN_COLORS_LIGHT } from '@ui/theme/constants/MainColorsLight'; export const SNACK_BAR_LIGHT = { success: { diff --git a/packages/twenty-ui/src/theme/constants/TagDark.ts b/packages/twenty-ui/src/theme/constants/TagDark.ts index 7c4f2fe1b5..ba2014fd3c 100644 --- a/packages/twenty-ui/src/theme/constants/TagDark.ts +++ b/packages/twenty-ui/src/theme/constants/TagDark.ts @@ -1,4 +1,4 @@ -import { COLOR_DARK } from '@new-ui/theme/constants/ColorsDark'; +import { COLOR_DARK } from '@ui/theme/constants/ColorsDark'; export const TAG_DARK = { text: { diff --git a/packages/twenty-ui/src/theme/constants/TagLight.ts b/packages/twenty-ui/src/theme/constants/TagLight.ts index 0b450a8151..3a8c3d2c5b 100644 --- a/packages/twenty-ui/src/theme/constants/TagLight.ts +++ b/packages/twenty-ui/src/theme/constants/TagLight.ts @@ -1,4 +1,4 @@ -import { COLOR_LIGHT } from '@new-ui/theme/constants/ColorsLight'; +import { COLOR_LIGHT } from '@ui/theme/constants/ColorsLight'; export const TAG_LIGHT = { text: { diff --git a/packages/twenty-ui/src/theme/constants/ThemeCommon.ts b/packages/twenty-ui/src/theme/constants/ThemeCommon.ts index 60f7dc28e5..d1b1a4bf14 100644 --- a/packages/twenty-ui/src/theme/constants/ThemeCommon.ts +++ b/packages/twenty-ui/src/theme/constants/ThemeCommon.ts @@ -1,4 +1,4 @@ -import { ACCENT_DARK } from '@new-ui/theme/constants/AccentDark'; +import { ACCENT_DARK } from '@ui/theme/constants/AccentDark'; import { ANIMATION } from './Animation'; import { ICON } from './Icon'; import { MODAL } from './Modal'; diff --git a/packages/twenty-ui/src/theme/constants/ThemeDark.ts b/packages/twenty-ui/src/theme/constants/ThemeDark.ts index 385c63ddde..94cf05ea05 100644 --- a/packages/twenty-ui/src/theme/constants/ThemeDark.ts +++ b/packages/twenty-ui/src/theme/constants/ThemeDark.ts @@ -1,5 +1,5 @@ -import { BLUR_DARK } from '@new-ui/theme/constants/BlurDark'; -import { ILLUSTRATION_ICON_DARK } from '@new-ui/theme/constants/IllustrationIconDark'; +import { BLUR_DARK } from '@ui/theme/constants/BlurDark'; +import { ILLUSTRATION_ICON_DARK } from '@ui/theme/constants/IllustrationIconDark'; import { COLOR_DARK } from './ColorsDark'; import { GRAY_SCALE_DARK } from './GrayScaleDark'; import { SNACK_BAR_DARK } from './SnackBarDark'; diff --git a/packages/twenty-ui/src/theme/constants/ThemeLight.ts b/packages/twenty-ui/src/theme/constants/ThemeLight.ts index 1968e514b2..930d0fbd30 100644 --- a/packages/twenty-ui/src/theme/constants/ThemeLight.ts +++ b/packages/twenty-ui/src/theme/constants/ThemeLight.ts @@ -1,8 +1,8 @@ -import { BLUR_LIGHT } from '@new-ui/theme/constants/BlurLight'; -import { COLOR_LIGHT } from '@new-ui/theme/constants/ColorsLight'; -import { GRAY_SCALE_LIGHT } from '@new-ui/theme/constants/GrayScaleLight'; -import { ILLUSTRATION_ICON_LIGHT } from '@new-ui/theme/constants/IllustrationIconLight'; -import { SNACK_BAR_LIGHT } from '@new-ui/theme/constants/SnackBarLight'; +import { BLUR_LIGHT } from '@ui/theme/constants/BlurLight'; +import { COLOR_LIGHT } from '@ui/theme/constants/ColorsLight'; +import { GRAY_SCALE_LIGHT } from '@ui/theme/constants/GrayScaleLight'; +import { ILLUSTRATION_ICON_LIGHT } from '@ui/theme/constants/IllustrationIconLight'; +import { SNACK_BAR_LIGHT } from '@ui/theme/constants/SnackBarLight'; import { ACCENT_LIGHT } from './AccentLight'; import { BACKGROUND_LIGHT } from './BackgroundLight'; import { BORDER_LIGHT } from './BorderLight'; diff --git a/packages/twenty-ui/src/theme/constants/TransparentColorsDark.ts b/packages/twenty-ui/src/theme/constants/TransparentColorsDark.ts index dad6596485..c3aaa82c94 100644 --- a/packages/twenty-ui/src/theme/constants/TransparentColorsDark.ts +++ b/packages/twenty-ui/src/theme/constants/TransparentColorsDark.ts @@ -1,5 +1,5 @@ import * as RadixColors from '@radix-ui/colors'; -import { GRAY_SCALE_DARK_ALPHA } from '@new-ui/theme/constants/GrayScaleDarkAlpha'; +import { GRAY_SCALE_DARK_ALPHA } from '@ui/theme/constants/GrayScaleDarkAlpha'; export const TRANSPARENT_COLORS_DARK = { green1: RadixColors.greenDarkA.greenA1, diff --git a/packages/twenty-ui/src/theme/constants/TransparentColorsLight.ts b/packages/twenty-ui/src/theme/constants/TransparentColorsLight.ts index 93de485453..22d50526ac 100644 --- a/packages/twenty-ui/src/theme/constants/TransparentColorsLight.ts +++ b/packages/twenty-ui/src/theme/constants/TransparentColorsLight.ts @@ -1,5 +1,5 @@ import * as RadixColors from '@radix-ui/colors'; -import { GRAY_SCALE_LIGHT_ALPHA } from '@new-ui/theme/constants/GrayScaleLightAlpha'; +import { GRAY_SCALE_LIGHT_ALPHA } from '@ui/theme/constants/GrayScaleLightAlpha'; export const TRANSPARENT_COLORS_LIGHT = { green1: RadixColors.greenA.greenA1, diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedCircleLoading.module.scss b/packages/twenty-ui/src/utilities/animation/components/AnimatedCircleLoading.module.scss new file mode 100644 index 0000000000..d32734b929 --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedCircleLoading.module.scss @@ -0,0 +1,5 @@ +.container { + align-items: center; + display: flex; + justify-content: center; +} diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedCircleLoading.module.scss.d.ts b/packages/twenty-ui/src/utilities/animation/components/AnimatedCircleLoading.module.scss.d.ts new file mode 100644 index 0000000000..8df71b4dea --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedCircleLoading.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly container: 'container'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedCircleLoading.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedCircleLoading.tsx new file mode 100644 index 0000000000..9bb35c6eef --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedCircleLoading.tsx @@ -0,0 +1,31 @@ +import { motion } from 'framer-motion'; +import React, { useContext } from 'react'; + +import { ThemeContext } from '@ui/theme-constants'; + +import styles from './AnimatedCircleLoading.module.scss'; + +export const AnimatedCircleLoading = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { theme } = useContext(ThemeContext); + + return ( + + {children} + + ); +}; diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedContainer.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedContainer.tsx new file mode 100644 index 0000000000..581e03cb7b --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedContainer.tsx @@ -0,0 +1,17 @@ +import { motion } from 'framer-motion'; +import React from 'react'; + +export const AnimatedContainer = ({ + children, +}: { + children: React.ReactNode; +}) => ( + + {children} + +); diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedEaseIn.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedEaseIn.tsx new file mode 100644 index 0000000000..2548b0aff0 --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedEaseIn.tsx @@ -0,0 +1,31 @@ +import { motion } from 'framer-motion'; +import { useContext } from 'react'; +import { type AnimationDuration } from '@ui/theme'; +import { ThemeContext } from '@ui/theme-constants'; + +type AnimatedEaseInProps = Omit< + React.ComponentProps, + 'initial' | 'animated' | 'transition' +> & { + duration?: AnimationDuration; +}; + +export const AnimatedEaseIn = ({ + children, + duration = 'normal', +}: AnimatedEaseInProps) => { + const { theme } = useContext(ThemeContext); + + const initial = { opacity: 0 }; + const animate = { opacity: 1 }; + const transition = { + ease: 'linear', + duration: theme.animation.duration[duration], + }; + + return ( + + {children} + + ); +}; diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedEaseInOut.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedEaseInOut.tsx new file mode 100644 index 0000000000..9754b0d1d2 --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedEaseInOut.tsx @@ -0,0 +1,47 @@ +import { AnimatePresence, motion } from 'framer-motion'; +import { useContext } from 'react'; +import { type AnimationDuration } from '@ui/theme'; +import { ThemeContext } from '@ui/theme-constants'; + +type AnimatedEaseInOutProps = { + isOpen: boolean; + children: React.ReactNode; + duration?: AnimationDuration; + marginBottom?: string; + marginTop?: string; + initial?: boolean; +}; + +export const AnimatedEaseInOut = ({ + children, + isOpen, + marginBottom, + marginTop, + duration = 'normal', + initial = true, +}: AnimatedEaseInOutProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + {isOpen && ( + + {children} + + )} + + ); +}; diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedFadeOut.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedFadeOut.tsx new file mode 100644 index 0000000000..d3e3ffb35d --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedFadeOut.tsx @@ -0,0 +1,43 @@ +import { AnimatePresence, motion } from 'framer-motion'; +import { useContext } from 'react'; +import { type AnimationDuration } from '@ui/theme'; +import { ThemeContext } from '@ui/theme-constants'; + +type AnimatedFadeOutProps = { + isOpen: boolean; + children: React.ReactNode; + duration?: AnimationDuration; + marginBottom?: string; + marginTop?: string; +}; + +export const AnimatedFadeOut = ({ + isOpen, + children, + duration = 'normal', + marginBottom, + marginTop, +}: AnimatedFadeOutProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + {isOpen && ( + + {children} + + )} + + ); +}; diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedIconCrossfade.module.scss b/packages/twenty-ui/src/utilities/animation/components/AnimatedIconCrossfade.module.scss new file mode 100644 index 0000000000..dd8851c0af --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedIconCrossfade.module.scss @@ -0,0 +1,14 @@ +.container { + height: var(--animated-icon-crossfade-size); + overflow: hidden; + position: relative; + width: var(--animated-icon-crossfade-size); +} + +.layer { + align-items: center; + display: flex; + inset: 0; + justify-content: center; + position: absolute; +} diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedIconCrossfade.module.scss.d.ts b/packages/twenty-ui/src/utilities/animation/components/AnimatedIconCrossfade.module.scss.d.ts new file mode 100644 index 0000000000..defbe83142 --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedIconCrossfade.module.scss.d.ts @@ -0,0 +1,5 @@ +declare const classNames: { + readonly container: 'container'; + readonly layer: 'layer'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedIconCrossfade.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedIconCrossfade.tsx new file mode 100644 index 0000000000..a91d695c49 --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedIconCrossfade.tsx @@ -0,0 +1,65 @@ +import { motion } from 'framer-motion'; +import React, { useContext } from 'react'; + +import { type IconComponent } from '@ui/display/icon/types/IconComponent'; +import { ThemeContext } from '@ui/theme-constants'; + +import styles from './AnimatedIconCrossfade.module.scss'; + +type AnimatedIconCrossfadeProps = { + isActive: boolean; + ActiveIcon: IconComponent; + InactiveIcon: IconComponent; + size?: number; +}; + +export const AnimatedIconCrossfade = ({ + isActive, + ActiveIcon, + InactiveIcon, + size, +}: AnimatedIconCrossfadeProps) => { + const { theme } = useContext(ThemeContext); + + const iconSize = size ?? theme.icon.size.sm; + + return ( +
    + + + + + + +
    + ); +}; diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedRotate.module.scss b/packages/twenty-ui/src/utilities/animation/components/AnimatedRotate.module.scss new file mode 100644 index 0000000000..d32734b929 --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedRotate.module.scss @@ -0,0 +1,5 @@ +.container { + align-items: center; + display: flex; + justify-content: center; +} diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedRotate.module.scss.d.ts b/packages/twenty-ui/src/utilities/animation/components/AnimatedRotate.module.scss.d.ts new file mode 100644 index 0000000000..8df71b4dea --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedRotate.module.scss.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly container: 'container'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedRotate.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedRotate.tsx new file mode 100644 index 0000000000..ab72c43845 --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedRotate.tsx @@ -0,0 +1,51 @@ +import { type HTMLMotionProps, motion } from 'framer-motion'; +import { type AnimationDuration } from '@ui/theme'; +import { ThemeContext } from '@ui/theme-constants'; +import { useContext } from 'react'; + +import styles from './AnimatedRotate.module.scss'; + +type AnimatedRotateProps = Omit< + HTMLMotionProps<'div'>, + 'initial' | 'animate' | 'transition' | 'exit' +> & { + duration?: AnimationDuration; + animateOnHover?: boolean; +}; + +export const AnimatedRotate = ({ + children, + duration = 'fast', + animateOnHover, +}: AnimatedRotateProps): JSX.Element => { + const { theme } = useContext(ThemeContext); + + const initial = { opacity: 0, rotate: -90 }; + const animate = { opacity: 1, rotate: 0 }; + const exit = { opacity: 0, rotate: 90 }; + const transition = { + duration: theme.animation.duration[duration], + }; + + return ( + + {children} + + ); +}; diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedTextWord.module.scss b/packages/twenty-ui/src/utilities/animation/components/AnimatedTextWord.module.scss new file mode 100644 index 0000000000..0c3791924f --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedTextWord.module.scss @@ -0,0 +1,8 @@ +.container { + display: flex; + overflow: hidden; +} + +.word { + white-space: pre; +} diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedTextWord.module.scss.d.ts b/packages/twenty-ui/src/utilities/animation/components/AnimatedTextWord.module.scss.d.ts new file mode 100644 index 0000000000..79778847e0 --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedTextWord.module.scss.d.ts @@ -0,0 +1,5 @@ +declare const classNames: { + readonly container: 'container'; + readonly word: 'word'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedTextWord.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedTextWord.tsx new file mode 100644 index 0000000000..1b82b35bc2 --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedTextWord.tsx @@ -0,0 +1,69 @@ +import { motion } from 'framer-motion'; +import React, { useMemo } from 'react'; + +import styles from './AnimatedTextWord.module.scss'; + +type AnimatedTextWordProps = Omit< + React.ComponentProps, + 'children' +> & { + text: string; +}; + +const containerAnimation = { + hidden: { opacity: 0 }, + visible: (i = 1) => ({ + opacity: 1, + transition: { staggerChildren: 0.12, delayChildren: 0.04 * i }, + }), +}; + +const childAnimation = { + visible: { + opacity: 1, + x: 0, + transition: { + type: 'spring', + damping: 12, + stiffness: 100, + }, + }, + hidden: { + opacity: 0, + x: 20, + transition: { + type: 'spring', + damping: 12, + stiffness: 100, + }, + }, +}; + +export const AnimatedTextWord = ({ text = '' }: AnimatedTextWordProps) => { + const words = useMemo(() => { + const words = text.split(' '); + + return words.map((value, index) => + index === words.length - 1 ? value : value + ' ', + ); + }, [text]); + + return ( + + {words.map((word, index) => ( + + {word} + + ))} + + ); +}; diff --git a/packages/twenty-ui/src/utilities/animation/components/AnimatedTranslation.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedTranslation.tsx new file mode 100644 index 0000000000..03e226f643 --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedTranslation.tsx @@ -0,0 +1,34 @@ +import { motion } from 'framer-motion'; +import { useContext } from 'react'; +import { ThemeContext } from '@ui/theme-constants'; + +type AnimatedTranslationProps = { + children: React.ReactNode; +}; + +export const AnimatedTranslation = ({ children }: AnimatedTranslationProps) => { + const { theme } = useContext(ThemeContext); + + return ( + + {children} + + ); +}; diff --git a/packages/twenty-ui/src/utilities/animation/index.ts b/packages/twenty-ui/src/utilities/animation/index.ts new file mode 100644 index 0000000000..1754312639 --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/index.ts @@ -0,0 +1,8 @@ +export * from './components/AnimatedCircleLoading'; +export * from './components/AnimatedContainer'; +export * from './components/AnimatedEaseIn'; +export * from './components/AnimatedEaseInOut'; +export * from './components/AnimatedFadeOut'; +export * from './components/AnimatedIconCrossfade'; +export * from './components/AnimatedTextWord'; +export * from './components/AnimatedTranslation'; diff --git a/packages/twenty-ui/src/utilities/color/utils/__tests__/getMainColorNameFromPaletteColorName.test.ts b/packages/twenty-ui/src/utilities/color/utils/__tests__/getMainColorNameFromPaletteColorName.test.ts new file mode 100644 index 0000000000..85c2766454 --- /dev/null +++ b/packages/twenty-ui/src/utilities/color/utils/__tests__/getMainColorNameFromPaletteColorName.test.ts @@ -0,0 +1,48 @@ +import { getMainColorNameFromPaletteColorName } from '../getMainColorNameFromPaletteColorName'; + +describe('getMainColorNameFromPaletteColorName', () => { + it('should extract main color name from palette color names', () => { + expect(getMainColorNameFromPaletteColorName('purple5')).toBe('purple'); + expect(getMainColorNameFromPaletteColorName('blue8')).toBe('blue'); + expect(getMainColorNameFromPaletteColorName('red3')).toBe('red'); + expect(getMainColorNameFromPaletteColorName('orange12')).toBe('orange'); + expect(getMainColorNameFromPaletteColorName('yellow1')).toBe('yellow'); + expect(getMainColorNameFromPaletteColorName('crimson9')).toBe('crimson'); + expect(getMainColorNameFromPaletteColorName('violet7')).toBe('violet'); + expect(getMainColorNameFromPaletteColorName('iris4')).toBe('iris'); + expect(getMainColorNameFromPaletteColorName('grass6')).toBe('grass'); + expect(getMainColorNameFromPaletteColorName('mint5')).toBe('mint'); + expect(getMainColorNameFromPaletteColorName('lime3')).toBe('lime'); + expect(getMainColorNameFromPaletteColorName('bronze8')).toBe('bronze'); + expect(getMainColorNameFromPaletteColorName('gold2')).toBe('gold'); + expect(getMainColorNameFromPaletteColorName('pink1')).toBe('pink'); + expect(getMainColorNameFromPaletteColorName('turquoise2')).toBe( + 'turquoise', + ); + expect(getMainColorNameFromPaletteColorName('green10')).toBe('green'); + expect(getMainColorNameFromPaletteColorName('cyan11')).toBe('cyan'); + expect(getMainColorNameFromPaletteColorName('jade12')).toBe('jade'); + }); + + it('should handle color names without numbers', () => { + expect(getMainColorNameFromPaletteColorName('purple')).toBe('purple'); + expect(getMainColorNameFromPaletteColorName('blue')).toBe('blue'); + expect(getMainColorNameFromPaletteColorName('red')).toBe('red'); + expect(getMainColorNameFromPaletteColorName('orange')).toBe('orange'); + expect(getMainColorNameFromPaletteColorName('yellow')).toBe('yellow'); + expect(getMainColorNameFromPaletteColorName('crimson')).toBe('crimson'); + expect(getMainColorNameFromPaletteColorName('violet')).toBe('violet'); + expect(getMainColorNameFromPaletteColorName('iris')).toBe('iris'); + expect(getMainColorNameFromPaletteColorName('grass')).toBe('grass'); + expect(getMainColorNameFromPaletteColorName('mint')).toBe('mint'); + expect(getMainColorNameFromPaletteColorName('lime')).toBe('lime'); + expect(getMainColorNameFromPaletteColorName('bronze')).toBe('bronze'); + expect(getMainColorNameFromPaletteColorName('gold')).toBe('gold'); + expect(getMainColorNameFromPaletteColorName('pink')).toBe('pink'); + expect(getMainColorNameFromPaletteColorName('turquoise')).toBe('turquoise'); + expect(getMainColorNameFromPaletteColorName('green')).toBe('green'); + expect(getMainColorNameFromPaletteColorName('cyan')).toBe('cyan'); + expect(getMainColorNameFromPaletteColorName('jade')).toBe('jade'); + expect(getMainColorNameFromPaletteColorName('gray')).toBe('gray'); + }); +}); diff --git a/packages/twenty-ui/src/utilities/color/utils/__tests__/stringToThemeColorP3String.test.ts b/packages/twenty-ui/src/utilities/color/utils/__tests__/stringToThemeColorP3String.test.ts new file mode 100644 index 0000000000..c7f3ba9ca0 --- /dev/null +++ b/packages/twenty-ui/src/utilities/color/utils/__tests__/stringToThemeColorP3String.test.ts @@ -0,0 +1,36 @@ +import { stringToThemeColor } from '@ui/utilities'; +import { stringToThemeColorP3String } from '@ui/utilities/color/utils/stringToThemeColorP3String'; + +describe('stringToThemeColor', () => { + it('should return a theme color based on a string', () => { + const color = stringToThemeColor('test'); + expect(typeof color).toBe('string'); + expect(color).toMatch( + /^(gray|tomato|red|ruby|crimson|pink|plum|purple|violet|iris|cyan|turquoise|sky|blue|jade|green|grass|mint|lime|bronze|gold|brown|orange|amber|yellow)$/, + ); + }); + + it('should return the same color for the same string', () => { + const color1 = stringToThemeColor('test'); + const color2 = stringToThemeColor('test'); + expect(color1).toBe(color2); + }); +}); + +describe('stringToThemeColorP3String', () => { + it('should return a resolved CSS variable value for a color', () => { + const mockTheme = { + color: new Proxy({} as Record, { + get: (_, prop) => `mock-${String(prop)}`, + }), + } as any; + + const color = stringToThemeColorP3String({ + string: 'test', + variant: 9, + theme: mockTheme, + }); + + expect(typeof color).toBe('string'); + }); +}); diff --git a/packages/twenty-ui/src/utilities/color/utils/getMainColorNameFromPaletteColorName.ts b/packages/twenty-ui/src/utilities/color/utils/getMainColorNameFromPaletteColorName.ts new file mode 100644 index 0000000000..9d5694028c --- /dev/null +++ b/packages/twenty-ui/src/utilities/color/utils/getMainColorNameFromPaletteColorName.ts @@ -0,0 +1,7 @@ +import { type ThemeColor } from '@ui/theme'; + +export const getMainColorNameFromPaletteColorName = ( + paletteColorName: string, +): ThemeColor => { + return paletteColorName.replace(/\d+$/, '') as ThemeColor; +}; diff --git a/packages/twenty-ui/src/utilities/color/utils/parseThemeColor.ts b/packages/twenty-ui/src/utilities/color/utils/parseThemeColor.ts new file mode 100644 index 0000000000..f6a3d656b7 --- /dev/null +++ b/packages/twenty-ui/src/utilities/color/utils/parseThemeColor.ts @@ -0,0 +1,10 @@ +import { DEFAULT_THEME_COLOR_FALLBACK, type ThemeColor } from '@ui/theme'; + +import { themeColorSchema } from './themeColorSchema'; + +export const parseThemeColor = ( + color: string | null | undefined, +): ThemeColor => { + const result = themeColorSchema.safeParse(color ?? ''); + return result.success ? result.data : DEFAULT_THEME_COLOR_FALLBACK; +}; diff --git a/packages/twenty-ui/src/utilities/color/utils/stringToThemeColorP3String.ts b/packages/twenty-ui/src/utilities/color/utils/stringToThemeColorP3String.ts new file mode 100644 index 0000000000..c01a5e12c6 --- /dev/null +++ b/packages/twenty-ui/src/utilities/color/utils/stringToThemeColorP3String.ts @@ -0,0 +1,34 @@ +import { type ThemeType, type themeCssVariables } from '@ui/theme-constants'; +import { type ThemeColor, MAIN_COLOR_NAMES } from '@ui/theme'; + +export const stringToThemeColor = (string: string): ThemeColor => { + let hash = 0; + for (let i = 0; i < string.length; i++) { + hash = string.charCodeAt(i) + ((hash << 5) - hash); + } + + const colorIndex = Math.abs(hash) % MAIN_COLOR_NAMES.length; + return MAIN_COLOR_NAMES[colorIndex]; +}; + +export const stringToThemeColorP3String = ({ + string, + variant = 9, + theme, +}: { + string: string; + variant: number; + theme: ThemeType; +}): string => { + if (variant < 1 || variant > 12) { + throw new Error('Variant must be between 1 and 12'); + } + + const colorName = stringToThemeColor(string); + const colorNameWithVariant = + `${colorName}${variant}` as keyof typeof themeCssVariables.color; + + return theme.color[ + colorNameWithVariant as keyof typeof theme.color + ] as string; +}; diff --git a/packages/twenty-ui/src/utilities/color/utils/themeColorSchema.ts b/packages/twenty-ui/src/utilities/color/utils/themeColorSchema.ts new file mode 100644 index 0000000000..8b2bde570c --- /dev/null +++ b/packages/twenty-ui/src/utilities/color/utils/themeColorSchema.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +import { type ThemeColor, MAIN_COLOR_NAMES } from '@ui/theme'; + +export const themeColorSchema = z.enum( + MAIN_COLOR_NAMES as [ThemeColor, ...ThemeColor[]], +); diff --git a/packages/twenty-ui/src/utilities/config.ts b/packages/twenty-ui/src/utilities/config.ts new file mode 100644 index 0000000000..91782a631e --- /dev/null +++ b/packages/twenty-ui/src/utilities/config.ts @@ -0,0 +1,21 @@ +const getDefaultUrl = () => { + if ( + window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1' + ) { + // In development environment front and backend usually run on separate ports + // we set the default value to localhost:3000. + // In dev context, we use env vars to overwrite it + return 'http://localhost:3000'; + } else { + // Outside of localhost we assume that they run on the same port + // because the backend will serve the frontend + // In prod context, we use index.html + window var to ovewrite it + return `${window.location.protocol}//${window.location.hostname}${ + window.location.port ? `:${window.location.port}` : '' + }`; + } +}; + +export const REACT_APP_SERVER_BASE_URL = + window._env_?.REACT_APP_SERVER_BASE_URL || getDefaultUrl(); diff --git a/packages/twenty-ui/src/utilities/device/__tests__/getOsControlSymbol.test.ts b/packages/twenty-ui/src/utilities/device/__tests__/getOsControlSymbol.test.ts new file mode 100644 index 0000000000..bd97e08a35 --- /dev/null +++ b/packages/twenty-ui/src/utilities/device/__tests__/getOsControlSymbol.test.ts @@ -0,0 +1,27 @@ +import { getOsControlSymbol } from '../getOsControlSymbol'; + +describe('getOsControlSymbol', () => { + let userAgentSpy: jest.SpyInstance; + + beforeEach(() => { + userAgentSpy = jest.spyOn(window.navigator, 'userAgent', 'get'); + }); + + afterEach(() => { + userAgentSpy.mockRestore(); + }); + + it('should return Ctrl for Windows', () => { + userAgentSpy.mockReturnValue( + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + ); + expect(getOsControlSymbol()).toBe('Ctrl'); + }); + + it('should return ⌘ for Mac', () => { + userAgentSpy.mockReturnValue( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + ); + expect(getOsControlSymbol()).toBe('⌘'); + }); +}); diff --git a/packages/twenty-ui/src/utilities/device/__tests__/getOsShortcutSeparator.test.ts b/packages/twenty-ui/src/utilities/device/__tests__/getOsShortcutSeparator.test.ts new file mode 100644 index 0000000000..16205be142 --- /dev/null +++ b/packages/twenty-ui/src/utilities/device/__tests__/getOsShortcutSeparator.test.ts @@ -0,0 +1,27 @@ +import { getOsShortcutSeparator } from '../getOsShortcutSeparator'; + +describe('getOsShortcutSeparator', () => { + let userAgentSpy: jest.SpyInstance; + + beforeEach(() => { + userAgentSpy = jest.spyOn(window.navigator, 'userAgent', 'get'); + }); + + afterEach(() => { + userAgentSpy.mockRestore(); + }); + + it('should return space for Windows', () => { + userAgentSpy.mockReturnValue( + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + ); + expect(getOsShortcutSeparator()).toBe(' '); + }); + + it('should return empty string for Mac', () => { + userAgentSpy.mockReturnValue( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + ); + expect(getOsShortcutSeparator()).toBe(''); + }); +}); diff --git a/packages/twenty-ui/src/utilities/device/getOsControlSymbol.ts b/packages/twenty-ui/src/utilities/device/getOsControlSymbol.ts new file mode 100644 index 0000000000..3278c3501e --- /dev/null +++ b/packages/twenty-ui/src/utilities/device/getOsControlSymbol.ts @@ -0,0 +1,7 @@ +import { getUserDevice } from '@ui/utilities/device/getUserDevice'; + +export const getOsControlSymbol = () => { + const device = getUserDevice(); + + return device === 'mac' ? '⌘' : 'Ctrl'; +}; diff --git a/packages/twenty-ui/src/utilities/device/getOsShortcutSeparator.ts b/packages/twenty-ui/src/utilities/device/getOsShortcutSeparator.ts new file mode 100644 index 0000000000..c7bc029b8e --- /dev/null +++ b/packages/twenty-ui/src/utilities/device/getOsShortcutSeparator.ts @@ -0,0 +1,6 @@ +import { getUserDevice } from '@ui/utilities/device/getUserDevice'; + +export const getOsShortcutSeparator = () => { + const device = getUserDevice(); + return device === 'mac' ? '' : ' '; +}; diff --git a/packages/twenty-ui/src/utilities/device/getUserDevice.ts b/packages/twenty-ui/src/utilities/device/getUserDevice.ts new file mode 100644 index 0000000000..1e724e079b --- /dev/null +++ b/packages/twenty-ui/src/utilities/device/getUserDevice.ts @@ -0,0 +1,26 @@ +export const getUserDevice = () => { + const userAgent = navigator.userAgent.toLowerCase(); + + if (userAgent.includes('mac os x') || userAgent.includes('macos')) { + return 'mac'; + } + + if (userAgent.includes('windows')) { + return 'windows'; + } + + if (userAgent.includes('linux')) { + return 'linux'; + } + + if (userAgent.includes('android')) return 'android'; + + if ( + userAgent.includes('ios') || + userAgent.includes('iphone') || + userAgent.includes('ipad') + ) + return 'ios'; + + return 'unknown'; +}; diff --git a/packages/twenty-ui/src/utilities/dimensions/components/AutogrowWrapper.module.scss b/packages/twenty-ui/src/utilities/dimensions/components/AutogrowWrapper.module.scss new file mode 100644 index 0000000000..444896e83f --- /dev/null +++ b/packages/twenty-ui/src/utilities/dimensions/components/AutogrowWrapper.module.scss @@ -0,0 +1,17 @@ +.container { + max-width: 100%; + position: relative; +} + +.nodeWrapper { + pointer-events: none; + visibility: hidden; + white-space: pre; +} + +.childWrapper { + left: 0; + top: 0; + position: absolute; + width: 100%; +} diff --git a/packages/twenty-ui/src/utilities/dimensions/components/AutogrowWrapper.module.scss.d.ts b/packages/twenty-ui/src/utilities/dimensions/components/AutogrowWrapper.module.scss.d.ts new file mode 100644 index 0000000000..8489fa3143 --- /dev/null +++ b/packages/twenty-ui/src/utilities/dimensions/components/AutogrowWrapper.module.scss.d.ts @@ -0,0 +1,6 @@ +declare const classNames: { + readonly container: 'container'; + readonly nodeWrapper: 'nodeWrapper'; + readonly childWrapper: 'childWrapper'; +}; +export default classNames; diff --git a/packages/twenty-ui/src/utilities/dimensions/components/AutogrowWrapper.tsx b/packages/twenty-ui/src/utilities/dimensions/components/AutogrowWrapper.tsx new file mode 100644 index 0000000000..da0dd5926d --- /dev/null +++ b/packages/twenty-ui/src/utilities/dimensions/components/AutogrowWrapper.tsx @@ -0,0 +1,23 @@ +import { clsx } from 'clsx'; +import { type ReactNode } from 'react'; + +import styles from './AutogrowWrapper.module.scss'; + +type AutogrowWrapperProps = { + children: ReactNode; + node?: ReactNode; + className?: string; +}; + +export const AutogrowWrapper = ({ + children, + node = children, + className, +}: AutogrowWrapperProps) => { + return ( +
    + {node} +
    {children}
    +
    + ); +}; diff --git a/packages/twenty-ui/src/utilities/index.ts b/packages/twenty-ui/src/utilities/index.ts index a3046804c0..202e2d2b53 100644 --- a/packages/twenty-ui/src/utilities/index.ts +++ b/packages/twenty-ui/src/utilities/index.ts @@ -7,4 +7,38 @@ * |___/ */ -export {}; +export { AnimatedCircleLoading } from './animation/components/AnimatedCircleLoading'; +export { AnimatedContainer } from './animation/components/AnimatedContainer'; +export { AnimatedEaseIn } from './animation/components/AnimatedEaseIn'; +export { AnimatedEaseInOut } from './animation/components/AnimatedEaseInOut'; +export { AnimatedFadeOut } from './animation/components/AnimatedFadeOut'; +export { AnimatedIconCrossfade } from './animation/components/AnimatedIconCrossfade'; +export { AnimatedRotate } from './animation/components/AnimatedRotate'; +export { AnimatedTextWord } from './animation/components/AnimatedTextWord'; +export { AnimatedTranslation } from './animation/components/AnimatedTranslation'; +export { getMainColorNameFromPaletteColorName } from './color/utils/getMainColorNameFromPaletteColorName'; +export { parseThemeColor } from './color/utils/parseThemeColor'; +export { + stringToThemeColor, + stringToThemeColorP3String, +} from './color/utils/stringToThemeColorP3String'; +export { themeColorSchema } from './color/utils/themeColorSchema'; +export { REACT_APP_SERVER_BASE_URL } from './config'; +export { getOsControlSymbol } from './device/getOsControlSymbol'; +export { getOsShortcutSeparator } from './device/getOsShortcutSeparator'; +export { getUserDevice } from './device/getUserDevice'; +export { AutogrowWrapper } from './dimensions/components/AutogrowWrapper'; +export { useMouseDownNavigation } from './navigation/hooks/useMouseDownNavigation'; +export { useResetLocationHash } from './navigation/hooks/useResetLocationHash'; +export { isNavigationModifierPressed } from './navigation/isNavigationModifierPressed'; +export type { TriggerEventType } from './navigation/types/trigger-event.type'; +export { useIsMobile } from './responsive/hooks/useIsMobile'; +export { useScreenSize } from './screen-size/hooks/useScreenSize'; +export { createState } from './state/utils/createState'; +export type { ClickOutsideAttributes } from './types/ClickOutsideAttributes'; +export type { Nullable } from './types/Nullable'; +export { getDisplayValueByUrlType } from './utils/getDisplayValueByUrlType'; +export { getImageAbsoluteURI } from './utils/getImageAbsoluteURI'; +export { isDefined } from './utils/isDefined'; +export type { LinkifyMatch } from './utils/linkifyText'; +export { linkifyText } from './utils/linkifyText'; diff --git a/packages/twenty-ui/src/utilities/navigation/hooks/useMouseDownNavigation.ts b/packages/twenty-ui/src/utilities/navigation/hooks/useMouseDownNavigation.ts new file mode 100644 index 0000000000..75f3c3ba27 --- /dev/null +++ b/packages/twenty-ui/src/utilities/navigation/hooks/useMouseDownNavigation.ts @@ -0,0 +1,71 @@ +import { isNavigationModifierPressed } from '@ui/utilities/navigation/isNavigationModifierPressed'; +import { type TriggerEventType } from '@ui/utilities/navigation/types/trigger-event.type'; +import { type MouseEvent } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +type UseMouseDownNavigationProps = { + to?: string; + onClick?: (event: MouseEvent) => void; + disabled?: boolean; + onBeforeNavigation?: () => void; + triggerEvent?: TriggerEventType; + stopPropagation?: boolean; +}; + +export const useMouseDownNavigation = ({ + to, + onClick, + disabled = false, + onBeforeNavigation, + triggerEvent = 'MOUSE_DOWN', +}: UseMouseDownNavigationProps) => { + const navigate = useNavigate(); + + const handleClick = (event: MouseEvent) => { + if (disabled) return; + + // For modifier keys, let the default browser behavior handle it + if (isNavigationModifierPressed(event)) { + onBeforeNavigation?.(); + if (isDefined(onClick) && !isDefined(to)) { + onClick(event); + } + // Don't prevent default for modifier keys to allow browser navigation + return; + } + + if (triggerEvent === 'CLICK') { + onBeforeNavigation?.(); + if (isDefined(onClick)) { + onClick(event); + } else if (isDefined(to)) { + navigate(to); + } + } + + // For regular clicks, prevent default to avoid double navigation + event.preventDefault(); + }; + + const handleMouseDown = (event: MouseEvent) => { + if (disabled || triggerEvent === 'CLICK') return; + + if (isNavigationModifierPressed(event)) { + return; + } + + onBeforeNavigation?.(); + + if (isDefined(onClick)) { + onClick(event); + } else if (isDefined(to)) { + navigate(to); + } + }; + + return { + onClick: handleClick, + onMouseDown: handleMouseDown, + }; +}; diff --git a/packages/twenty-ui/src/utilities/navigation/hooks/useResetLocationHash.ts b/packages/twenty-ui/src/utilities/navigation/hooks/useResetLocationHash.ts new file mode 100644 index 0000000000..a24fcdbac4 --- /dev/null +++ b/packages/twenty-ui/src/utilities/navigation/hooks/useResetLocationHash.ts @@ -0,0 +1,13 @@ +import { useLocation, useNavigate } from 'react-router-dom'; + +export const useResetLocationHash = () => { + const navigate = useNavigate(); + const location = useLocation(); + + // oxlint-disable-next-line twenty/no-navigate-prefer-link + const resetLocationHash = () => { + navigate(location.pathname, { replace: true }); + }; + + return { resetLocationHash }; +}; diff --git a/packages/twenty-ui/src/utilities/navigation/isNavigationModifierPressed.ts b/packages/twenty-ui/src/utilities/navigation/isNavigationModifierPressed.ts new file mode 100644 index 0000000000..92398ceb43 --- /dev/null +++ b/packages/twenty-ui/src/utilities/navigation/isNavigationModifierPressed.ts @@ -0,0 +1,16 @@ +type LimitedMouseEvent = Pick< + MouseEvent, + 'button' | 'metaKey' | 'altKey' | 'ctrlKey' | 'shiftKey' +>; + +export const isNavigationModifierPressed = ({ + altKey, + ctrlKey, + shiftKey, + metaKey, + button, +}: LimitedMouseEvent) => { + const pressedKey = [altKey, ctrlKey, shiftKey, metaKey].some((key) => key); + const isLeftClick = button === 0; + return pressedKey || !isLeftClick; +}; diff --git a/packages/twenty-ui/src/utilities/navigation/types/trigger-event.type.ts b/packages/twenty-ui/src/utilities/navigation/types/trigger-event.type.ts new file mode 100644 index 0000000000..ea2294ba9c --- /dev/null +++ b/packages/twenty-ui/src/utilities/navigation/types/trigger-event.type.ts @@ -0,0 +1 @@ +export type TriggerEventType = 'MOUSE_DOWN' | 'CLICK'; diff --git a/packages/twenty-ui/src/utilities/responsive/hooks/__tests__/isMobile.test.tsx b/packages/twenty-ui/src/utilities/responsive/hooks/__tests__/isMobile.test.tsx new file mode 100644 index 0000000000..708733136b --- /dev/null +++ b/packages/twenty-ui/src/utilities/responsive/hooks/__tests__/isMobile.test.tsx @@ -0,0 +1,10 @@ +import { renderHook } from '@testing-library/react'; +import { useIsMobile } from '@ui/utilities/responsive/hooks/useIsMobile'; + +describe('useIsMobile', () => { + it('should trigger the callback when clicking outside the specified refs', () => { + const { result } = renderHook(() => useIsMobile()); + + expect(result.current).toBe(false); + }); +}); diff --git a/packages/twenty-ui/src/utilities/responsive/hooks/useIsMobile.ts b/packages/twenty-ui/src/utilities/responsive/hooks/useIsMobile.ts new file mode 100644 index 0000000000..cc734c964f --- /dev/null +++ b/packages/twenty-ui/src/utilities/responsive/hooks/useIsMobile.ts @@ -0,0 +1,5 @@ +import { MOBILE_VIEWPORT } from '@ui/theme-constants'; +import { useMediaQuery } from 'react-responsive'; + +export const useIsMobile = () => + useMediaQuery({ query: `(max-width: ${MOBILE_VIEWPORT}px)` }); diff --git a/packages/twenty-ui/src/utilities/screen-size/hooks/useScreenSize.ts b/packages/twenty-ui/src/utilities/screen-size/hooks/useScreenSize.ts new file mode 100644 index 0000000000..2363af5dbf --- /dev/null +++ b/packages/twenty-ui/src/utilities/screen-size/hooks/useScreenSize.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; + +export const useScreenSize = () => { + const [screenSize, setScreenSize] = useState({ + width: window.innerWidth, + height: window.innerHeight, + }); + + useEffect(() => { + const handleResize = () => { + setScreenSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + return screenSize; +}; diff --git a/packages/twenty-ui/src/utilities/state/utils/createState.ts b/packages/twenty-ui/src/utilities/state/utils/createState.ts new file mode 100644 index 0000000000..220424f24c --- /dev/null +++ b/packages/twenty-ui/src/utilities/state/utils/createState.ts @@ -0,0 +1,13 @@ +import { atom, type PrimitiveAtom } from 'jotai'; + +export const createState = ({ + key, + defaultValue, +}: { + key: string; + defaultValue: ValueType; +}): PrimitiveAtom => { + const jotaiAtom = atom(defaultValue); + jotaiAtom.debugLabel = key; + return jotaiAtom; +}; diff --git a/packages/twenty-ui/src/utilities/types/ClickOutsideAttributes.ts b/packages/twenty-ui/src/utilities/types/ClickOutsideAttributes.ts new file mode 100644 index 0000000000..39f538b575 --- /dev/null +++ b/packages/twenty-ui/src/utilities/types/ClickOutsideAttributes.ts @@ -0,0 +1,4 @@ +export type ClickOutsideAttributes = { + dataClickOutsideId?: string; + dataGloballyPreventClickOutside?: boolean; +}; diff --git a/packages/twenty-ui/src/utilities/types/Nullable.ts b/packages/twenty-ui/src/utilities/types/Nullable.ts new file mode 100644 index 0000000000..164a7056d9 --- /dev/null +++ b/packages/twenty-ui/src/utilities/types/Nullable.ts @@ -0,0 +1 @@ +export type Nullable = T | null | undefined; diff --git a/packages/twenty-ui/src/utilities/utils/__tests__/getDisplayValueByUrlType.test.ts b/packages/twenty-ui/src/utilities/utils/__tests__/getDisplayValueByUrlType.test.ts new file mode 100644 index 0000000000..2aa2e162f8 --- /dev/null +++ b/packages/twenty-ui/src/utilities/utils/__tests__/getDisplayValueByUrlType.test.ts @@ -0,0 +1,149 @@ +import { LinkType } from '@ui/navigation/link/components/SocialLink'; + +import { getDisplayValueByUrlType } from '../getDisplayValueByUrlType'; + +describe('getDisplayValueByUrlType', () => { + describe('linkedin', () => { + it('should extract username from LinkedIn profile URL', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.LinkedIn, + href: 'https://www.linkedin.com/in/johndoe', + }); + expect(result).toBe('johndoe'); + }); + + it('should extract company name from LinkedIn company URL', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.LinkedIn, + href: 'https://www.linkedin.com/company/acme-corp', + }); + expect(result).toBe('acme-corp'); + }); + + it('should extract school name from LinkedIn school URL', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.LinkedIn, + href: 'https://www.linkedin.com/school/mit', + }); + expect(result).toBe('mit'); + }); + + it('should handle LinkedIn URL without protocol', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.LinkedIn, + href: 'linkedin.com/in/johndoe', + }); + expect(result).toBe('johndoe'); + }); + + it('should handle LinkedIn URL without www', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.LinkedIn, + href: 'https://linkedin.com/in/johndoe', + }); + expect(result).toBe('johndoe'); + }); + + it('should decode URL-encoded characters in LinkedIn username', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.LinkedIn, + href: 'https://www.linkedin.com/in/john%20doe', + }); + expect(result).toBe('john doe'); + }); + + it('should return "LinkedIn" for invalid LinkedIn URLs', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.LinkedIn, + href: 'https://www.linkedin.com/feed', + }); + expect(result).toBe('LinkedIn'); + }); + }); + + describe('twitter', () => { + it('should extract username from Twitter URL with @ prefix', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.Twitter, + href: 'https://www.twitter.com/johndoe', + }); + expect(result).toBe('@johndoe'); + }); + + it('should handle Twitter URL without protocol', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.Twitter, + href: 'twitter.com/johndoe', + }); + expect(result).toBe('@johndoe'); + }); + + it('should handle Twitter URL without www', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.Twitter, + href: 'https://twitter.com/johndoe', + }); + expect(result).toBe('@johndoe'); + }); + + it('should return "@twitter" when no username can be extracted', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.Twitter, + href: 'https://www.twitter.com', + }); + expect(result).toBe('@twitter'); + }); + }); + + describe('facebook', () => { + it('should extract username from Facebook profile URL', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.Facebook, + href: 'https://www.facebook.com/johndoe', + }); + expect(result).toBe('johndoe'); + }); + + it('should handle Facebook URL without protocol', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.Facebook, + href: 'facebook.com/johndoe', + }); + expect(result).toBe('johndoe'); + }); + + it('should handle Facebook URL without www', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.Facebook, + href: 'https://facebook.com/johndoe', + }); + expect(result).toBe('johndoe'); + }); + + it('should decode URL-encoded characters in Facebook username', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.Facebook, + href: 'https://www.facebook.com/john%20doe', + }); + expect(result).toBe('john doe'); + }); + + it('should return "Facebook" for invalid Facebook URLs', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.Facebook, + href: 'https://www.facebook.com/', + }); + expect(result).toBe('Facebook'); + }); + }); + + describe('url type', () => { + it('should return undefined for generic url type', () => { + const result = getDisplayValueByUrlType({ + type: LinkType.Url, + href: 'https://example.com', + }); + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/packages/twenty-ui/src/utilities/utils/__tests__/linkifyText.test.ts b/packages/twenty-ui/src/utilities/utils/__tests__/linkifyText.test.ts new file mode 100644 index 0000000000..3548dae567 --- /dev/null +++ b/packages/twenty-ui/src/utilities/utils/__tests__/linkifyText.test.ts @@ -0,0 +1,51 @@ +import { linkifyText } from '../linkifyText'; + +describe('linkifyText', () => { + it('splits text around a URL into text and link segments', () => { + expect(linkifyText('visit https://example.com today')).toEqual([ + { type: 'text', content: 'visit ' }, + { type: 'link', content: 'https://example.com' }, + { type: 'text', content: ' today' }, + ]); + }); + + it('returns text as-is when no URLs are present', () => { + expect(linkifyText('no links here')).toEqual([ + { type: 'text', content: 'no links here' }, + ]); + }); + + it('parses Teams meeting descriptions with angle-bracket URLs and encoded characters', () => { + const teamsDescription = + 'Need help? | System reference'; + + const result = linkifyText(teamsDescription); + + expect(result).toEqual([ + { type: 'text', content: 'Need help?<' }, + { + type: 'link', + content: 'https://aka.ms/JoinTeamsMeeting?omkt=en-GB', + }, + { type: 'text', content: '> | System reference<' }, + { + type: 'link', + content: + 'https://teams.microsoft.com/l/meetup-join/19%3ameeting_abc%40thread.v2', + }, + { type: 'text', content: '>' }, + ]); + }); + + it('strips trailing punctuation from matched URLs', () => { + expect(linkifyText('see https://example.com, or this.')).toEqual([ + { type: 'text', content: 'see ' }, + { type: 'link', content: 'https://example.com' }, + { type: 'text', content: ', or this.' }, + ]); + }); + + it('returns empty array for empty string', () => { + expect(linkifyText('')).toEqual([]); + }); +}); diff --git a/packages/twenty-ui/src/utilities/utils/getDisplayValueByUrlType.ts b/packages/twenty-ui/src/utilities/utils/getDisplayValueByUrlType.ts new file mode 100644 index 0000000000..fa704d0a24 --- /dev/null +++ b/packages/twenty-ui/src/utilities/utils/getDisplayValueByUrlType.ts @@ -0,0 +1,43 @@ +import { type LinkType } from '@ui/navigation'; +import { isDefined } from '@ui/utilities/utils/isDefined'; + +type getUrlDisplayValueByUrlTypeProps = { + type: LinkType; + href: string; +}; + +export const getDisplayValueByUrlType = ({ + type, + href, +}: getUrlDisplayValueByUrlTypeProps) => { + if (type === 'linkedin') { + const matches = href.match( + /(?:https?:\/\/)?(?:www.)?linkedin.com\/(?:in|company|school)\/(.*)/, + ); + if (isDefined(matches?.[1])) { + return decodeURIComponent(matches?.[1]); + } else { + return 'LinkedIn'; + } + } + + if (type === 'twitter') { + const matches = href.match( + /(?:https?:\/\/)?(?:www.)?twitter.com\/([-a-zA-Z0-9@:%_+.~#?&//=]*)/, + ); + if (isDefined(matches?.[1])) { + return `@${matches?.[1]}`; + } else { + return '@twitter'; + } + } + + if (type === 'facebook') { + const matches = href.match(/(?:https?:\/\/)?(?:www.)?facebook.com\/(.+)/); + if (isDefined(matches?.[1])) { + return decodeURIComponent(matches?.[1]); + } else { + return 'Facebook'; + } + } +}; diff --git a/packages/twenty-ui/src/utilities/utils/getImageAbsoluteURI.ts b/packages/twenty-ui/src/utilities/utils/getImageAbsoluteURI.ts new file mode 100644 index 0000000000..8f041a4e4c --- /dev/null +++ b/packages/twenty-ui/src/utilities/utils/getImageAbsoluteURI.ts @@ -0,0 +1,19 @@ +type getImageAbsoluteURIProps = { + imageUrl: string; + baseUrl: string; +}; + +export const getImageAbsoluteURI = ({ + imageUrl, + baseUrl, +}: getImageAbsoluteURIProps): string => { + if (imageUrl.startsWith('https:') || imageUrl.startsWith('http:')) { + return imageUrl; + } + + if (imageUrl.startsWith('/')) { + return new URL(`/files${imageUrl}`, baseUrl).toString(); + } + + return new URL(`/files/${imageUrl}`, baseUrl).toString(); +}; diff --git a/packages/twenty-ui/src/utilities/utils/isDefined.ts b/packages/twenty-ui/src/utilities/utils/isDefined.ts new file mode 100644 index 0000000000..aa97cf342f --- /dev/null +++ b/packages/twenty-ui/src/utilities/utils/isDefined.ts @@ -0,0 +1,5 @@ +import { isNull, isUndefined } from '@sniptt/guards'; + +export const isDefined = ( + value: T | null | undefined, +): value is NonNullable => !isUndefined(value) && !isNull(value); diff --git a/packages/twenty-ui/src/utilities/utils/linkifyText.ts b/packages/twenty-ui/src/utilities/utils/linkifyText.ts new file mode 100644 index 0000000000..b03e14cf3e --- /dev/null +++ b/packages/twenty-ui/src/utilities/utils/linkifyText.ts @@ -0,0 +1,28 @@ +const URL_REGEX = /https?:\/\/[^\s<>[\]]+[^\s<>[\].,;:!?)]/g; + +export type LinkifyMatch = { + type: 'text' | 'link'; + content: string; +}; + +export const linkifyText = (text: string): LinkifyMatch[] => { + const parts: LinkifyMatch[] = []; + let lastIndex = 0; + + for (const match of text.matchAll(URL_REGEX)) { + const url = match[0]; + const index = match.index; + + if (index > lastIndex) { + parts.push({ type: 'text', content: text.slice(lastIndex, index) }); + } + parts.push({ type: 'link', content: url }); + lastIndex = index + url.length; + } + + if (lastIndex < text.length) { + parts.push({ type: 'text', content: text.slice(lastIndex) }); + } + + return parts; +}; diff --git a/packages/twenty-ui/tsconfig.json b/packages/twenty-ui/tsconfig.json index 7ff5429b60..b05f2b26b8 100644 --- a/packages/twenty-ui/tsconfig.json +++ b/packages/twenty-ui/tsconfig.json @@ -12,7 +12,7 @@ "types": ["node", "jest"], "outDir": "../../.cache/tsc", "paths": { - "@new-ui/*": ["./src/*"], + "@ui/*": ["./src/*"], "@assets/*": ["./src/assets/*"], "@styles/*": ["./src/styles/*"] } diff --git a/packages/twenty-ui/vite.config.individual.ts b/packages/twenty-ui/vite.config.individual.ts index 15f2b08ab3..d21fa425d1 100644 --- a/packages/twenty-ui/vite.config.individual.ts +++ b/packages/twenty-ui/vite.config.individual.ts @@ -15,7 +15,7 @@ export default defineConfig(() => { return { resolve: { alias: { - '@new-ui/': path.resolve(__dirname, 'src') + '/', + '@ui/': path.resolve(__dirname, 'src') + '/', '@assets/': path.resolve(__dirname, 'src/assets') + '/', '@styles/': path.resolve(__dirname, 'src/styles') + '/', }, diff --git a/packages/twenty-ui/vite.config.ts b/packages/twenty-ui/vite.config.ts index 9af45ff622..b0190e9cdd 100644 --- a/packages/twenty-ui/vite.config.ts +++ b/packages/twenty-ui/vite.config.ts @@ -64,7 +64,7 @@ export default defineConfig(({ command }) => { return { resolve: { alias: { - '@new-ui/': path.resolve(__dirname, 'src') + '/', + '@ui/': path.resolve(__dirname, 'src') + '/', '@assets/': path.resolve(__dirname, 'src/assets') + '/', '@styles/': path.resolve(__dirname, 'src/styles') + '/', '@tabler/icons-react': '@tabler/icons-react/dist/esm/icons/index.mjs', diff --git a/yarn.lock b/yarn.lock index dc1d8d335c..726c37644b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55455,6 +55455,7 @@ __metadata: "@prettier/sync": "npm:^0.5.2" "@radix-ui/colors": "npm:^3.0.0" "@size-limit/preset-small-lib": "npm:^11.1.6" + "@sniptt/guards": "npm:^0.2.0" "@storybook-community/storybook-addon-cookie": "npm:^5.0.0" "@storybook/addon-a11y": "npm:^10.3.3" "@storybook/addon-coverage": "npm:^3.0.0" @@ -55492,7 +55493,6 @@ __metadata: storybook-addon-pseudo-states: "npm:^10.3.3" ts-jest: "npm:^29.1.1" tsx: "npm:^4.19.3" - twenty-shared: "workspace:*" vite-plugin-checker: "npm:^0.10.2" vite-plugin-dts: "npm:^4.5.4" vite-plugin-sass-dts: "npm:^1.3.31"