├── .eslintrc.js ├── .github └── CODEOWNERS ├── .gitignore ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── demo-public └── index.html ├── jest.config.js ├── package.json ├── src ├── __snapshots__ │ └── storybook.test.js.snap ├── components │ ├── alert.js │ ├── alert.stories.js │ ├── alert.test.js │ ├── chip.js │ ├── chip.stories.js │ ├── chip.test.js │ ├── codeSample.js │ ├── codeSample.stories.js │ ├── codeSample.test.js │ ├── copyInput.js │ ├── copyInput.stories.js │ ├── copyInput.test.js │ ├── copyNotification │ │ ├── copyNotification.js │ │ ├── copyNotification.stories.js │ │ ├── copyNotification.test.js │ │ └── snackbarContent.js │ ├── iconButton.js │ ├── iconButton.stories.js │ ├── panel.js │ ├── panel.stories.js │ ├── roundedButton.js │ ├── roundedButton.stories.js │ ├── roundedButton.test.js │ ├── searchBar │ │ ├── overrides │ │ │ ├── input.js │ │ │ ├── searchInput.js │ │ │ ├── textField.js │ │ │ └── utils │ │ │ │ └── autosuggest-highlight.js │ │ ├── searchBar.js │ │ ├── searchBar.stories.js │ │ ├── searchBar.test.js │ │ └── searchBarJest.js │ ├── select.js │ ├── select.stories.js │ ├── tabs.js │ ├── tabs.stories.js │ ├── tabs.test.js │ ├── twoPanelLayout.js │ └── twoPanelLayout.stories.js ├── globalStyles │ ├── index.js │ └── lato │ │ ├── LatoLatin-Black.eot │ │ ├── LatoLatin-Black.ttf │ │ ├── LatoLatin-Black.woff │ │ ├── LatoLatin-Black.woff2 │ │ ├── LatoLatin-BlackItalic.eot │ │ ├── LatoLatin-BlackItalic.ttf │ │ ├── LatoLatin-BlackItalic.woff │ │ ├── LatoLatin-BlackItalic.woff2 │ │ ├── LatoLatin-Bold.eot │ │ ├── LatoLatin-Bold.ttf │ │ ├── LatoLatin-Bold.woff │ │ ├── LatoLatin-Bold.woff2 │ │ ├── LatoLatin-BoldItalic.eot │ │ ├── LatoLatin-BoldItalic.ttf │ │ ├── LatoLatin-BoldItalic.woff │ │ ├── LatoLatin-BoldItalic.woff2 │ │ ├── LatoLatin-Hairline.eot │ │ ├── LatoLatin-Hairline.ttf │ │ ├── LatoLatin-Hairline.woff │ │ ├── LatoLatin-Hairline.woff2 │ │ ├── LatoLatin-HairlineItalic.eot │ │ ├── LatoLatin-HairlineItalic.ttf │ │ ├── LatoLatin-HairlineItalic.woff │ │ ├── LatoLatin-HairlineItalic.woff2 │ │ ├── LatoLatin-Heavy.eot │ │ ├── LatoLatin-Heavy.ttf │ │ ├── LatoLatin-Heavy.woff │ │ ├── LatoLatin-Heavy.woff2 │ │ ├── LatoLatin-HeavyItalic.eot │ │ ├── LatoLatin-HeavyItalic.ttf │ │ ├── LatoLatin-HeavyItalic.woff │ │ ├── LatoLatin-HeavyItalic.woff2 │ │ ├── LatoLatin-Light.eot │ │ ├── LatoLatin-Light.ttf │ │ ├── LatoLatin-Light.woff │ │ ├── LatoLatin-Light.woff2 │ │ ├── LatoLatin-LightItalic.eot │ │ ├── LatoLatin-LightItalic.ttf │ │ ├── LatoLatin-LightItalic.woff │ │ ├── LatoLatin-LightItalic.woff2 │ │ ├── LatoLatin-Medium.eot │ │ ├── LatoLatin-Medium.ttf │ │ ├── LatoLatin-Medium.woff │ │ ├── LatoLatin-Medium.woff2 │ │ ├── LatoLatin-MediumItalic.eot │ │ ├── LatoLatin-MediumItalic.ttf │ │ ├── LatoLatin-MediumItalic.woff │ │ ├── LatoLatin-MediumItalic.woff2 │ │ ├── LatoLatin-Regular.eot │ │ ├── LatoLatin-Regular.ttf │ │ ├── LatoLatin-Regular.woff │ │ ├── LatoLatin-Regular.woff2 │ │ ├── LatoLatin-RegularItalic.eot │ │ ├── LatoLatin-RegularItalic.ttf │ │ ├── LatoLatin-RegularItalic.woff │ │ ├── LatoLatin-RegularItalic.woff2 │ │ ├── LatoLatin-Semibold.eot │ │ ├── LatoLatin-Semibold.ttf │ │ ├── LatoLatin-Semibold.woff │ │ ├── LatoLatin-Semibold.woff2 │ │ ├── LatoLatin-SemiboldItalic.eot │ │ ├── LatoLatin-SemiboldItalic.ttf │ │ ├── LatoLatin-SemiboldItalic.woff │ │ ├── LatoLatin-SemiboldItalic.woff2 │ │ ├── LatoLatin-Thin.eot │ │ ├── LatoLatin-Thin.ttf │ │ ├── LatoLatin-Thin.woff │ │ ├── LatoLatin-Thin.woff2 │ │ ├── LatoLatin-ThinItalic.eot │ │ ├── LatoLatin-ThinItalic.ttf │ │ ├── LatoLatin-ThinItalic.woff │ │ └── LatoLatin-ThinItalic.woff2 ├── icons │ ├── alert.js │ ├── algo.js │ ├── book.js │ ├── check.js │ ├── clear.js │ ├── clipboard.js │ ├── collapse.js │ ├── copyDrop.js │ ├── copySimple.js │ ├── dataset.js │ ├── downloadDrop.js │ ├── downloadSimple.js │ ├── expand.js │ ├── filterUp.js │ ├── folder.js │ ├── icons.stories.js │ ├── index.js │ ├── model.js │ ├── moreVertical.js │ ├── owkestraLogo.js │ ├── permission.js │ ├── search.js │ └── substraLogo.js ├── index.js ├── storybook.test.js ├── utils │ └── propTypes.js └── variables │ ├── colors.js │ ├── font.js │ └── spacing.js ├── test ├── mocks │ ├── fileMock.js │ ├── prismMock.js │ └── styleMock.js └── setup.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "indent": [0], 6 | "react/destructuring-assignment": [0], 7 | "react/jsx-indent": [2, 4], 8 | "react/jsx-indent-props": [2, 4], 9 | "react/jsx-filename-extension": [0], 10 | "react/jsx-fragments": [2, "element"], 11 | "react/jsx-closing-tag-location": [0], 12 | "react/jsx-props-no-spreading": [0], 13 | "react/no-unescaped-entities": [0], 14 | "react/no-danger": [0], 15 | "react/no-array-index-key": [0], 16 | "react/sort-comp": [0], 17 | "import/no-extraneous-dependencies": [0], 18 | "import/no-dynamic-require": [0], 19 | "import/extensions": [0], 20 | "camelcase": [0], 21 | "one-var": [0], 22 | "no-nested-ternary": [0], 23 | "brace-style": [2, 'stroustrup'], 24 | "no-confusing-arrow": [0], 25 | "object-property-newline": [2, {"allowMultiplePropertiesPerLine": true}], 26 | "no-shadow": [0], 27 | "global-require": [0], 28 | "no-console": [0, {"allow": ["warn", "error"]}], 29 | "no-lonely-if": [0], 30 | "no-restricted-syntax": [0], 31 | "no-underscore-dangle": [0], 32 | "import/no-webpack-loader-syntax": [0], 33 | "import/no-unresolved": [0], 34 | "import/no-useless-path-segments": [0], // for react-universal-component lazy loading 35 | "arrow-parens": [1, "as-needed"], 36 | "max-len": [0], 37 | "no-unused-vars": [2, {"args": "none"}], 38 | "consistent-return": [0], 39 | "no-bitwise": [0], 40 | "function-paren-newline": [0], 41 | "jsx-a11y/label-has-for": [0], 42 | "jsx-a11y/href-no-hash": "off", 43 | "jsx-a11y/anchor-is-valid": ["warn", { "aspects": ["invalidHref"] }], 44 | "jsx-a11y/click-events-have-key-events": [0], 45 | "jsx-a11y/no-static-element-interactions": [0], 46 | "no-multi-assign": [0], 47 | "no-mixed-operators": [0], 48 | "prefer-destructuring": [0], 49 | 50 | // helper for fixing common lint errors 51 | "func-names": [2], 52 | "import/first": [2], 53 | "import/newline-after-import": [2], 54 | "import/no-mutable-exports": [2], 55 | "import/prefer-default-export": [2], 56 | "react/forbid-prop-types": [2], 57 | "react/jsx-closing-bracket-location": [2], 58 | "react/jsx-no-bind": [2], 59 | "react/no-string-refs": [2], 60 | "react/no-unused-prop-types": [2], 61 | "react/prefer-es6-class": [2], 62 | "react/prefer-stateless-function": [2], 63 | "react/prop-types": [2], 64 | "no-param-reassign": [2], 65 | "no-undef": [2], 66 | 67 | // babel 68 | "object-curly-spacing": [0], 69 | "babel/object-curly-spacing": [2, 'never'], 70 | }, 71 | "plugins": [ 72 | "babel", 73 | "react", 74 | "jsx-a11y", 75 | "import", 76 | "jest", 77 | ], 78 | "env": { 79 | "jest/globals": true, 80 | }, 81 | "overrides": [ 82 | { 83 | "files": "**/*.spec.js", 84 | "rules": { 85 | "no-unused-expressions": [0], 86 | } 87 | } 88 | ] 89 | }; 90 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | # @global-owner1 and @global-owner2 will be requested for 4 | # review when someone opens a pull request. 5 | * @jmorel 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log* 3 | .idea/ 4 | es/ 5 | cjs/ 6 | yarn-error.log 7 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-storysource/register'; 2 | import '@storybook/addon-knobs/register'; 3 | import '@storybook/addon-actions/register'; 4 | import '@storybook/addon-links/register'; 5 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {configure, addDecorator} from '@storybook/react'; 3 | import requireContext from 'require-context.macro'; 4 | import {GlobalStyles} from "../src"; 5 | 6 | // automatically import all files ending in *.stories.js 7 | const req = requireContext('../src', true, /\.stories\.js$/); 8 | 9 | function loadStories() { 10 | req.keys().forEach(filename => req(filename)); 11 | } 12 | 13 | const withGlobalStyles = (cb) => ( 14 |
15 | 16 | {cb()} 17 |
18 | ); 19 | 20 | addDecorator(withGlobalStyles); 21 | 22 | 23 | configure(loadStories, module); 24 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | // you can use this file to add your custom webpack plugins, loaders and anything you like. 2 | // This is just the basic way to add additional webpack configurations. 3 | // For more information refer the docs: https://storybook.js.org/configurations/custom-webpack-config 4 | 5 | // IMPORTANT 6 | // When you add this file, we won't add the default configurations which is similar 7 | // to "React Create App". This only has babel loader to load JavaScript. 8 | 9 | module.exports = ({config}) => { 10 | config.module.rules.push({ 11 | test: /\.stories\.jsx?$/, 12 | loaders: [require.resolve('@storybook/addon-storysource/loader')], 13 | enforce: 'pre', 14 | }); 15 | 16 | return config; 17 | }; 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - lts/* 4 | 5 | deploy: 6 | provider: npm 7 | email: clement@gautier.im 8 | skip_cleanup: true 9 | api_key: 10 | secure: aTHnlgYOnpZ0HVYByrRaXH06sFhe7bPYye6YQG4Rh4gJj7YMmuw/c1SaAUj7fw5/ruuaV8zuD8aEZFV8E3icO2VI4YRP7c48JAEotizQVoj1QicPtEHev0y5SvsKiTWv6c1+PPAhItC3PChbV3W4L3JqQc6QkNlrmimWVDXjbobWjIKReM5gggTN3N2F1QA3B6WE+eXFENLORdiH1+MUaVnsJQ6ID8ZUo/Fi1uB0fG4PmepIoQKVwU1sA7kOumnKcvlibP2CHvOIP6Gb1inaO+/qNPWg/MOanWOHveRmYJvlBQghQwM0BbBGe6cwwOzzu/1KIcQEf8pctJwfbWtQAl6YHCv7lT6CJz19adonkCIaH7QI0ITfhnzsztDZ5cMFzDTt1eI2GvZIDQFgP5O/EZADB6OTKY7VvhL77g06q2ZOhhBrU+LPGn5kbFhoY4jLNwTtxcUM8YqbRdu1e9IjIZZGobT51kRSQElzRNr05Zw5NQJ3VNsrJiCGVThThWppaqiNA+eeHVX7H/i8pAGVCZZB5h9eYhIxZR6fnIRaVnCpx0SnjbUqf8FPxRRa2OUKhROsTrvKJyJHQVdU39vCmO2Eyh4TcLo4Rz65tnS6nsWv7UW9sbwPbeAuMp/FyEsiISnvD8XVP4PRZus9beVl2m6VAsDwGWPY5Na0DhOR6mM= 11 | on: 12 | tags: true 13 | repo: SubstraFoundation/substra-ui 14 | 15 | cache: yarn 16 | 17 | script: 18 | - yarn eslint 19 | - yarn test 20 | - yarn build 21 | - npm publish --dry-run 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2018-2019 Owkin, inc. 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Substra-ui :warning: DEPRECATED :warning: 2 | 3 | > The contents of this library have been merged in the main [substra-frontend](https://github.com/substrafoundation/substra-frontend/) repository. This library isn't maintained anymore and all future development will occur in the [substra-frontend](https://github.com/substrafoundation/substra-frontend/) repository. 4 | 5 | A shared UI components library for the Substra project. 6 | 7 | ## Storybook 8 | 9 | We use [Storybook](https://storybook.js.org/) for component development and testing: 10 | 11 | ```sh 12 | $ yarn storybook 13 | ``` 14 | 15 | ## Development setup 16 | 17 | Follow these steps to bypass the package repository and link together the local versions of `substra-ui` and `substrafront`. 18 | 19 | In the `substra-ui` directory: 20 | 21 | ```sh 22 | $ yarn link 23 | ``` 24 | 25 | In the `substrafront` directory: 26 | 27 | ```sh 28 | $ yarn link "@substrafoundation/substra-ui" 29 | $ yarn workspace ssr-package link "@substrafoundation/substra-ui" 30 | ``` 31 | 32 | Then you'll need to make your WIP content available to substrafront by either: 33 | * editing `package.json` in the `substra-ui` directory, changing `"module": "es/index.js",` into `"main": "src/index.js",` 34 | * or running `yarn build:es --watch` in the `substra-ui` directory 35 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | '@babel/preset-react', 5 | ], 6 | plugins: [ 7 | '@babel/plugin-proposal-export-default-from', 8 | '@babel/plugin-proposal-export-namespace-from', 9 | 'babel-plugin-macros', 10 | '@babel/plugin-proposal-class-properties', 11 | ], 12 | env: { 13 | es: { 14 | presets: [ 15 | [ 16 | '@babel/preset-env', 17 | { 18 | modules: false, 19 | }, 20 | ], 21 | ], 22 | }, 23 | cjs: { 24 | presets: [ 25 | [ 26 | '@babel/preset-env', 27 | ], 28 | ], 29 | }, 30 | }, 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /demo-public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Substra UI 8 | 9 | 10 | 11 |
12 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | snapshotSerializers: ['jest-emotion'], 3 | moduleNameMapper: { 4 | 'react-syntax-highlighter/dist/esm/styles/prism': '/test/mocks/prismMock.js', 5 | /* 6 | Taken from https://jestjs.io/docs/en/webpack#handling-static-assets 7 | Needed for handling the font files 8 | */ 9 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/test/mocks/fileMock.js', 10 | '\\.(css|less)$': '/test/mocks/styleMock.js' 11 | }, 12 | setupFilesAfterEnv: [ 13 | '/test/setup.js', 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@substrafoundation/substra-ui", 3 | "sideEffects": false, 4 | "version": "1.1.4", 5 | "description": "A shared UI components library for the Substra project.", 6 | "main": "cjs/index.js", 7 | "module": "es/index.js", 8 | "files": [ 9 | "es/", 10 | "cjs/" 11 | ], 12 | "repository": "git@github.com:SubstraFoundation/substra-ui.git", 13 | "author": "Jérémy Morel ", 14 | "license": "Apache-2.0", 15 | "private": false, 16 | "scripts": { 17 | "build:cjs": "NODE_ENV=cjs babel src --out-dir cjs --ignore **/*.test.js,**/*.stories.js", 18 | "build:es": "NODE_ENV=es babel src --out-dir es --ignore **/*.test.js,**/*.stories.js", 19 | "build": "npm run build:cjs && npm run build:es && cp -r src/globalStyles/lato cjs/globalStyles/ && cp -r src/globalStyles/lato es/globalStyles/", 20 | "eslint": "eslint --fix src", 21 | "eslint-check": "eslint src", 22 | "test": "jest src", 23 | "storybook": "start-storybook -p 6006", 24 | "build-storybook": "build-storybook" 25 | }, 26 | "dependencies": { 27 | "@emotion/core": "10.0.15", 28 | "@emotion/styled": "10.0.15", 29 | "copy-to-clipboard": "3.2.0", 30 | "diacritic": "0.0.2", 31 | "downshift": "3.2.12", 32 | "emotion": "10.0.14", 33 | "emotion-normalize": "10.1.0", 34 | "file-saver": "2.0.2", 35 | "keycode": "2.2.0", 36 | "mime-types": "2.1.24", 37 | "prop-types": "15.7.2", 38 | "react-syntax-highlighter": "11.0.2", 39 | "react-tabs": "3.0.0" 40 | }, 41 | "peerDependencies": { 42 | "react": "16.x", 43 | "react-is": "16.x" 44 | }, 45 | "devDependencies": { 46 | "@babel/cli": "7.5.5", 47 | "@babel/core": "7.5.5", 48 | "@babel/plugin-proposal-class-properties": "7.5.5", 49 | "@babel/plugin-proposal-export-default-from": "7.5.2", 50 | "@babel/plugin-proposal-export-namespace-from": "7.5.2", 51 | "@babel/preset-env": "7.5.5", 52 | "@babel/preset-react": "7.0.0", 53 | "@sambego/storybook-state": "1.3.6", 54 | "@storybook/addon-actions": "5.2.5", 55 | "@storybook/addon-knobs": "5.2.5", 56 | "@storybook/addon-links": "5.2.5", 57 | "@storybook/addon-storyshots": "5.2.5", 58 | "@storybook/addon-storysource": "5.2.5", 59 | "@storybook/addons": "5.2.5", 60 | "@storybook/cli": "5.2.5", 61 | "@storybook/react": "5.2.5", 62 | "@testing-library/jest-dom": "4.0.0", 63 | "@testing-library/react": "9.1.1", 64 | "@types/jest": "24.0.17", 65 | "babel-eslint": "10.0.2", 66 | "babel-jest": "24.8.0", 67 | "babel-loader": "8.0.6", 68 | "babel-plugin-emotion": "10.0.15", 69 | "babel-plugin-macros": "2.6.1", 70 | "eslint": "6.1.0", 71 | "eslint-config-airbnb": "18.0.0", 72 | "eslint-plugin-babel": "5.3.0", 73 | "eslint-plugin-import": "2.18.2", 74 | "eslint-plugin-jest": "22.15.1", 75 | "eslint-plugin-jsx-a11y": "6.2.3", 76 | "eslint-plugin-react": "7.14.3", 77 | "jest": "24.8.0", 78 | "jest-dom": "4.0.0", 79 | "jest-emotion": "10.0.14", 80 | "react-redux": "7.1.0", 81 | "react-test-renderer": "16.9.0", 82 | "redux": "4.0.4", 83 | "require-context.macro": "1.1.1" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/components/alert.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | import {gold, iceGold} from '../variables/colors'; 4 | import {spacingExtraSmall, spacingNormal, spacingSmall} from '../variables/spacing'; 5 | 6 | export const alertWrapper = ` 7 | background-color: ${iceGold}; 8 | border: 1px solid ${gold}; 9 | border-radius: 3px; 10 | min-height: 40px; 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: center; 14 | margin: ${spacingNormal} 0; 15 | flex-wrap: wrap; 16 | `; 17 | 18 | export const alertTitle = ` 19 | color: ${gold}; 20 | font-weight: bold; 21 | margin: ${spacingSmall}; 22 | `; 23 | 24 | export const AlertActions = styled('div')` 25 | margin: ${spacingExtraSmall} ${spacingExtraSmall} ${spacingExtraSmall} ${spacingSmall}; 26 | `; 27 | 28 | export const alertInlineButton = ` 29 | border: none; 30 | background: none; 31 | color: ${gold}; 32 | text-decoration: underline; 33 | cursor: pointer; 34 | padding: ${spacingExtraSmall}; 35 | `; 36 | -------------------------------------------------------------------------------- /src/components/alert.stories.js: -------------------------------------------------------------------------------- 1 | import {storiesOf} from '@storybook/react'; 2 | import React from 'react'; 3 | import {withKnobs, color} from '@storybook/addon-knobs'; 4 | import styled from '@emotion/styled'; 5 | import { 6 | alertInlineButton, 7 | AlertActions, 8 | alertTitle, 9 | alertWrapper, 10 | } from './alert'; 11 | import {darkSkyBlue, iceBlueTwo} from '../variables/colors'; 12 | 13 | storiesOf('Alert', module) 14 | .addDecorator(withKnobs) 15 | .add('default', () => { 16 | const AlertWrapper = styled('div')` 17 | ${alertWrapper} 18 | `; 19 | const AlertTitle = styled('div')` 20 | ${alertTitle} 21 | `; 22 | const AlertInlineButton = styled('button')` 23 | ${alertInlineButton} 24 | `; 25 | return ( 26 | 27 | This model has not been tested yet 28 | 29 | learn more 30 | 31 | 32 | ); 33 | }) 34 | .add('override', () => { 35 | const pColor = color('Primary color: ', iceBlueTwo); 36 | const sColor = color('Secondary color: ', darkSkyBlue); 37 | const AlertWrapper = styled('div')` 38 | ${alertWrapper}; 39 | background-color: ${pColor}; 40 | border: 1px solid ${sColor}; 41 | `; 42 | const AlertTitle = styled('div')` 43 | ${alertTitle}; 44 | color: ${sColor} 45 | `; 46 | const AlertInlineButton = styled('button')` 47 | ${alertInlineButton}; 48 | color: ${sColor} 49 | `; 50 | return ( 51 | 52 | This model has not been tested yet 53 | 54 | learn more 55 | 56 | 57 | ); 58 | }); 59 | -------------------------------------------------------------------------------- /src/components/alert.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import {render, fireEvent} from '@testing-library/react'; 4 | import { 5 | alertInlineButton, 6 | AlertActions, 7 | alertTitle, 8 | alertWrapper, 9 | } from './alert'; 10 | 11 | 12 | test('A function can be called by the button', () => { 13 | const AlertWrapper = styled('div')` 14 | ${alertWrapper} 15 | `; 16 | const AlertTitle = styled('div')` 17 | ${alertTitle} 18 | `; 19 | const AlertInlineButton = styled('button')` 20 | ${alertInlineButton} 21 | `; 22 | 23 | const mock = jest.fn(); 24 | 25 | const {getByTestId} = render( 26 | 27 | This model has not been tested yet 28 | 29 | learn more 30 | 31 | , 32 | ); 33 | 34 | expect(mock).not.toHaveBeenCalled(); 35 | fireEvent.click(getByTestId('button')); 36 | expect(mock).toHaveBeenCalledTimes(1); 37 | }); 38 | -------------------------------------------------------------------------------- /src/components/chip.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import {spacingSmall} from '../variables/spacing'; 4 | import PropTypes from '../utils/propTypes'; 5 | import {fontLarge} from '../variables/font'; 6 | import {darkSkyBlue} from '../variables/colors'; 7 | import {ClearIcon} from '../icons'; 8 | 9 | export const chipBackgroundColor = '#E0E0E0'; 10 | 11 | export const ChipWrapper = styled('span')` 12 | background-color: ${chipBackgroundColor}; 13 | border-radius: 30px; 14 | color: white; 15 | display: inline-flex; 16 | vertical-align: middle; 17 | margin: 3px 3px; 18 | `; 19 | 20 | export const ChipTitle = styled('div')` 21 | color: black; 22 | margin: auto; 23 | margin-left: ${spacingSmall}; 24 | font-size: ${fontLarge}; 25 | `; 26 | 27 | export const ChipActions = styled('div')` 28 | font-size: ${fontLarge}; 29 | `; 30 | 31 | export const ChipButtonStyle = styled.button` 32 | display: inline-flex; 33 | justify-content: center; 34 | width: 16px; 35 | height: 16px; 36 | border-radius: 15px; 37 | padding: 0; 38 | border: 1px; 39 | background-color: darkgray; 40 | cursor: pointer; 41 | outline: none; 42 | margin: 6px 6px; // define the internal margin of the chip 43 | 44 | &:hover { 45 | background-color: gray; 46 | transition: background-color 200ms ease-out; 47 | } 48 | &:focus { 49 | box-shadow: 0 0 3pt 3pt ${darkSkyBlue}; 50 | } 51 | `; 52 | const DefaultIcon = props => ; 53 | 54 | export const ChipButton = ({ 55 | Icon, iconSize, ...props 56 | }) => ( 57 | 58 | 59 | 60 | ); 61 | 62 | 63 | ChipButton.propTypes = { 64 | Icon: PropTypes.component, 65 | iconSize: PropTypes.number, 66 | }; 67 | 68 | ChipButton.defaultProps = { 69 | Icon: DefaultIcon, 70 | iconSize: 15, 71 | }; 72 | -------------------------------------------------------------------------------- /src/components/chip.stories.js: -------------------------------------------------------------------------------- 1 | import {storiesOf} from '@storybook/react'; 2 | import React from 'react'; 3 | import { 4 | ChipActions, ChipButton, ChipTitle, ChipWrapper, 5 | } from './chip'; 6 | 7 | storiesOf('Chip', module) 8 | .add('default', () => ( 9 | 10 | objective:key:1cdafbb018dd195690111d74916b76c9 11 | 12 | 13 | 14 | 15 | )); 16 | -------------------------------------------------------------------------------- /src/components/chip.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, fireEvent} from '@testing-library/react'; 3 | import { 4 | ChipActions, ChipButton, ChipTitle, ChipWrapper, 5 | } from './chip'; 6 | import Clear from '../icons/clear'; 7 | 8 | test('A function can be called by the button', () => { 9 | const mock = jest.fn(); 10 | 11 | const {getByTestId} = render( 12 | 13 | objective:key:1cdafbb018dd195690111d74916b76c9 14 | 15 | 16 | 17 | , 18 | ); 19 | expect(mock).not.toHaveBeenCalled(); 20 | fireEvent.click(getByTestId('button')); 21 | expect(mock).toHaveBeenCalledTimes(1); 22 | }); 23 | -------------------------------------------------------------------------------- /src/components/codeSample.js: -------------------------------------------------------------------------------- 1 | /* global Blob */ 2 | import React, {Component} from 'react'; 3 | import {css} from 'emotion'; 4 | import styled from '@emotion/styled'; 5 | import mime from 'mime-types'; 6 | import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter'; 7 | import {ghcolors} from 'react-syntax-highlighter/dist/esm/styles/prism'; 8 | import {saveAs} from 'file-saver'; 9 | import PropTypes from '../utils/propTypes'; 10 | import {fontNormalMonospace, monospaceFamily} from '../variables/font'; 11 | import {spacingExtraSmall, spacingNormal, spacingSmall} from '../variables/spacing'; 12 | import {ice, iceBlue} from '../variables/colors'; 13 | import {IconButton} from './iconButton'; 14 | import {Collapse, DownloadSimple, Expand} from '../icons'; 15 | 16 | const customStyle = { 17 | ...ghcolors, 18 | 'pre[class*="language-"]': { 19 | ...ghcolors['pre[class*="language-"]'], 20 | border: 'none', 21 | margin: 0, 22 | padding: `${spacingSmall}`, 23 | fontFamily: monospaceFamily, 24 | fontSize: fontNormalMonospace, 25 | }, 26 | 'code[class*="language-"]': { 27 | ...ghcolors['code[class*="language-"]'], 28 | fontFamily: monospaceFamily, 29 | fontSize: fontNormalMonospace, 30 | }, 31 | }; 32 | 33 | const lineNumberStyle = { 34 | userSelect: 'none', 35 | }; 36 | 37 | const FilenameWrapper = styled('div')` 38 | padding: ${spacingExtraSmall} ${spacingNormal}; 39 | `; 40 | 41 | const ActionsWrapper = styled('div')` 42 | padding: ${spacingExtraSmall}; 43 | `; 44 | 45 | const marginLeft = css` 46 | margin-left: ${spacingExtraSmall}; 47 | `; 48 | 49 | class CodeSample extends Component { 50 | state = { 51 | collapsed: true, 52 | }; 53 | 54 | downloadCode = e => { 55 | e.stopPropagation(); 56 | const {codeString, filename} = this.props; 57 | const jsonBlob = new Blob([codeString], {type: mime.lookup(filename) || 'text/plain'}); 58 | saveAs(jsonBlob, filename); 59 | }; 60 | 61 | toggleCollapsed = e => { 62 | e.stopPropagation(); 63 | this.setState(state => ({collapsed: !state.collapsed})); 64 | }; 65 | 66 | render() { 67 | const { 68 | filename, language, codeString, collapsible, className, 69 | } = this.props; 70 | const {collapsed} = this.state; 71 | 72 | const Wrapper = styled('div')` 73 | border: 1px solid ${ice}; 74 | border-radius: 3px; 75 | display: flex; 76 | flex-direction: column; 77 | ${collapsible && collapsed && 'max-height: 150px;'} 78 | `; 79 | 80 | const Header = styled('div')` 81 | display: flex; 82 | justify-content: space-between; 83 | align-items: center; 84 | border-bottom: 1px solid ${ice}; 85 | background-color: ${iceBlue}; 86 | min-height: 40px; 87 | font-family: ${monospaceFamily}; 88 | font-size: ${fontNormalMonospace}; 89 | ${collapsible && 'cursor: pointer;'} 90 | `; 91 | 92 | return ( 93 | 94 |
95 | 96 | {filename} 97 | 98 | 99 | 105 | {collapsible && ( 106 | 113 | )} 114 | 115 |
116 | 122 | {codeString} 123 | 124 |
125 | ); 126 | } 127 | } 128 | 129 | CodeSample.propTypes = { 130 | codeString: PropTypes.string.isRequired, 131 | filename: PropTypes.string.isRequired, 132 | language: PropTypes.string.isRequired, 133 | collapsible: PropTypes.bool, 134 | className: PropTypes.string, 135 | }; 136 | 137 | CodeSample.defaultProps = { 138 | collapsible: false, 139 | className: '', 140 | }; 141 | 142 | export default CodeSample; 143 | -------------------------------------------------------------------------------- /src/components/codeSample.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {storiesOf} from '@storybook/react'; 3 | 4 | import CodeSample from './codeSample'; 5 | 6 | const text = ` 7 | import os 8 | import pandas as pd 9 | import random 10 | import string 11 | import numpy as np 12 | import logging 13 | 14 | import substratools as tools 15 | 16 | 17 | class Opener(tools.Opener): 18 | def get_X(self, folders): 19 | logging.info(folders) 20 | datas = [] 21 | for folder in folders: 22 | datas.append(np.load('{}/X.npy'.format(folder))) 23 | return np.concatenate(datas, axis=0) 24 | 25 | def get_y(self, folders): 26 | y = [] 27 | for folder in folders: 28 | y.append(np.load('{}/y.npy'.format(folder))) 29 | return np.concatenate(y, axis=0) 30 | 31 | def save_pred(self, y_pred, path): 32 | with open(path, 'wb') as f: 33 | np.save(f, y_pred) 34 | 35 | def get_pred(self, path): 36 | return np.load(path) 37 | 38 | def fake_X(self): 39 | data = np.random.random((5, 1000, 2051)) 40 | return data 41 | 42 | def fake_y(self): 43 | y = np.random.randint(0, high=2, size=(5,)) 44 | return y 45 | `; 46 | 47 | storiesOf('CodeSample', module) 48 | .add('default', () => ( 49 | 50 | )) 51 | .add('collapsible', () => ( 52 | 53 | )); 54 | -------------------------------------------------------------------------------- /src/components/codeSample.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, fireEvent} from '@testing-library/react'; 3 | import {saveAs} from 'file-saver'; 4 | import CodeSample from './codeSample'; 5 | 6 | const codeString = 'toto'; // the default text used in the tests below 7 | const filename = 'opener.py'; // the default filename used in the tests below 8 | const language = 'python'; // the default language used in the tests below 9 | 10 | /* requirements for the "Download on click" test */ 11 | jest.mock('file-saver', () => ({saveAs: jest.fn()})); 12 | 13 | test('Change collapse/expand status on click', () => { 14 | const {getByTestId} = render(); 15 | let button = getByTestId('toggle'); 16 | expect(button.title).toEqual('Expand'); // check if the button has the title "Expand" 17 | let wrapper = getByTestId('wrapper'); 18 | expect(wrapper).toHaveStyle('max-height: 150px'); // check if the wrapper is collapsed through the css 19 | fireEvent.click(button); // simulate a click 20 | button = getByTestId('toggle'); 21 | expect(button.title).toEqual('Collapse'); // check if the button has the title "Collapse" 22 | wrapper = getByTestId('wrapper'); 23 | expect(wrapper).not.toHaveStyle('max-height: 150px'); // check if the wrapper is expanded through the css 24 | }); 25 | 26 | test('Download on click', () => { 27 | const {getByTestId} = render(); 28 | const button = getByTestId('download'); 29 | fireEvent.click(button); // simulate a click 30 | expect(saveAs).toHaveBeenCalledTimes(1); // then we verify that the saveAs function was correctly called one time only 31 | expect(saveAs).toHaveBeenCalledWith({content: [codeString], options: {type: 'text/plain'}}, filename); // check the params the download was called with 32 | }); 33 | 34 | test('Has a collapse/expand button', () => { 35 | const noButtonCpt = render(); 36 | expect(() => { 37 | noButtonCpt.getByTestId('toggle'); 38 | }).toThrow(); 39 | const withButtonCpt = render(); 40 | expect(withButtonCpt.getByTestId('toggle')).toBeTruthy(); 41 | }); 42 | -------------------------------------------------------------------------------- /src/components/copyInput.js: -------------------------------------------------------------------------------- 1 | import React, {Component, Fragment} from 'react'; 2 | import styled from '@emotion/styled'; 3 | import {css} from 'emotion'; 4 | import {noop} from 'lodash'; 5 | 6 | import PropTypes from '../utils/propTypes'; 7 | import {CopySimple, Check} from '../icons'; 8 | import {IconButton} from './iconButton'; 9 | import {ice, tealish} from '../variables/colors'; 10 | import {fontNormalMonospace, monospaceFamily} from '../variables/font'; 11 | import {spacingSmall} from '../variables/spacing'; 12 | 13 | 14 | const Wrapper = styled('div')` 15 | position: relative; 16 | `; 17 | 18 | const Input = styled('input')` 19 | width: 100%; 20 | background-color: transparent; 21 | color: inherit; 22 | font-family: ${monospaceFamily}; 23 | font-size: ${fontNormalMonospace}; 24 | text-overflow: ellipsis; 25 | border: 1px solid ${ice}; 26 | height: 30px; 27 | border-radius: 15px; 28 | line-height: 28px; 29 | padding-left: ${({isPrompt}) => isPrompt ? `calc(${spacingSmall} + 1em)` : spacingSmall}; 30 | padding-right: 35px; 31 | `; 32 | 33 | const button = css` 34 | position: absolute; 35 | top: 0; 36 | right: 0; 37 | `; 38 | 39 | const prompt = css` 40 | position: absolute; 41 | top: 0; 42 | left: 0; 43 | height: 30px; 44 | line-height: 30px; 45 | padding-left: ${spacingSmall}; 46 | pointer-events: none; 47 | `; 48 | 49 | const Prompt = () =>
$
; 50 | 51 | const DefaultSuccessIcon = props => ; 52 | 53 | class CopyInput extends Component { 54 | state = { 55 | clicked: false, 56 | }; 57 | 58 | constructor(props) { 59 | super(props); 60 | this.inputRef = React.createRef(); 61 | } 62 | 63 | componentWillUnmount() { 64 | clearTimeout(this.timeout); 65 | } 66 | 67 | copy = () => { 68 | const {value, addNotification, addNotificationMessage} = this.props; 69 | addNotification(value, addNotificationMessage); 70 | // animate icon 71 | this.setState({clicked: true}); 72 | clearTimeout(this.timeout); 73 | this.timeout = setTimeout(() => { 74 | this.setState({clicked: false}); 75 | }, 2000); 76 | }; 77 | 78 | select = () => { 79 | this.inputRef.current.select(); 80 | }; 81 | 82 | icon = () => { 83 | const {value, CopyIcon, SuccessIcon} = this.props; 84 | const clicked = this.state.clicked; 85 | const hasChanged = value !== this.previousValue; 86 | if (hasChanged) { 87 | clearTimeout(this.timeout); 88 | } 89 | this.previousValue = value; 90 | 91 | const copySimple = css` 92 | opacity: ${!hasChanged && clicked ? 0 : 1}; 93 | ${!hasChanged && 'transition: opacity 200ms ease-out;'} 94 | `; 95 | 96 | const check = css` 97 | position: absolute; 98 | opacity: ${!hasChanged && clicked ? 1 : 0}; 99 | ${!hasChanged && 'transition: opacity 200ms ease-out;'} 100 | `; 101 | 102 | return ({width, height}) => ( 103 | 104 | 109 | 114 | 115 | ); 116 | }; 117 | 118 | render() { 119 | const {value, isPrompt} = this.props; 120 | return ( 121 | 122 | {isPrompt && } 123 | 131 | 137 | 138 | ); 139 | } 140 | } 141 | 142 | CopyInput.propTypes = { 143 | value: PropTypes.string, 144 | isPrompt: PropTypes.bool, 145 | addNotification: PropTypes.func, 146 | addNotificationMessage: PropTypes.string, 147 | CopyIcon: PropTypes.component, 148 | SuccessIcon: PropTypes.component, 149 | }; 150 | 151 | CopyInput.defaultProps = { 152 | value: '', 153 | isPrompt: false, 154 | addNotification: noop, 155 | addNotificationMessage: 'Copied!', 156 | CopyIcon: CopySimple, 157 | SuccessIcon: DefaultSuccessIcon, 158 | }; 159 | 160 | export default CopyInput; 161 | -------------------------------------------------------------------------------- /src/components/copyInput.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {storiesOf} from '@storybook/react'; 3 | import {withKnobs, text, boolean} from '@storybook/addon-knobs/react'; 4 | 5 | import CopyInput from './copyInput'; 6 | import Book from '../icons/book'; 7 | import Model from '../icons/model'; 8 | 9 | 10 | storiesOf('CopyInput', module) 11 | .addDecorator(withKnobs) 12 | .add('default', () => { 13 | const value = text('value', 'Lorem ipsum dolor sit amet'); 14 | const addNotificationMessage = text('addNotificationMessage'); 15 | const addNotification = (v, t) => console.log(`value: ${v}\nmessage: ${t}`); 16 | const isPrompt = boolean('isPrompt', false); 17 | 18 | return ( 19 | 25 | ); 26 | }) 27 | .add('color override', () => { 28 | const CopyIcon = props => ; 29 | const SuccessIcon = props => ; 30 | return ; 31 | }); 32 | -------------------------------------------------------------------------------- /src/components/copyInput.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, fireEvent} from '@testing-library/react'; 3 | 4 | import CopyInput from './copyInput'; 5 | 6 | test('It calls the addNotification method', () => { 7 | const addNotification = jest.fn(); 8 | const value = 'my value'; 9 | const addNotificationMessage = 'Not the default message'; 10 | 11 | const {getByTestId, rerender} = render( 12 | , 16 | ); 17 | expect(addNotification).not.toHaveBeenCalled(); 18 | fireEvent.click(getByTestId('button')); 19 | expect(addNotification).toHaveBeenCalledTimes(1); 20 | expect(addNotification).toHaveBeenLastCalledWith(value, 'Copied!'); 21 | 22 | rerender( 23 | , 28 | ); 29 | fireEvent.click(getByTestId('button')); 30 | expect(addNotification).toHaveBeenCalledTimes(2); 31 | expect(addNotification).toHaveBeenLastCalledWith(value, addNotificationMessage); 32 | }); 33 | 34 | test('It displays a prompt', () => { 35 | const {getByText, rerender} = render(); 36 | expect(getByText('$')).toBeDefined(); 37 | 38 | rerender(); 39 | expect(() => getByText('$')).toThrow(); 40 | }); 41 | -------------------------------------------------------------------------------- /src/components/copyNotification/copyNotification.js: -------------------------------------------------------------------------------- 1 | import React, {Component, Fragment} from 'react'; 2 | import {css} from 'emotion'; 3 | import copy from 'copy-to-clipboard'; 4 | 5 | import {Check} from '../../icons'; 6 | import SnackbarContent from './snackbarContent'; 7 | 8 | import {slate, tealish} from '../../variables/colors'; 9 | import PropTypes from '../../utils/propTypes'; 10 | 11 | export const middle = ` 12 | display: inline-block; 13 | vertical-align: top; 14 | `; 15 | 16 | export const snackbarContentCSS = ` 17 | color: ${tealish}; 18 | 19 | @media (min-width: 960px) { 20 | min-width: 200px; 21 | } 22 | `; 23 | 24 | export const clipboardContentCSS = ` 25 | ${middle}; 26 | margin-left: 15px; 27 | input { 28 | display: block; 29 | padding: 3px 0; 30 | border: 1px solid #9b9b9b; 31 | color: #9b9b9b; 32 | background-color: transparent; 33 | outline: none; 34 | width: 100%; 35 | } 36 | 37 | p { 38 | color: ${slate}; 39 | font-size: 13px; 40 | margin: 4px 0 0; 41 | } 42 | `; 43 | 44 | const withAddNotification = (WrappedComponent, Icon = Check) => { 45 | class CopyNotification extends Component { 46 | state = { 47 | open: false, 48 | key: '', 49 | text: '', 50 | }; 51 | 52 | componentWillUnmount() { 53 | if (this.timeout) { 54 | clearTimeout(this.timeout); 55 | } 56 | if (this.queueTimeout) { 57 | clearTimeout(this.queueTimeout); 58 | } 59 | } 60 | 61 | processNotificationQueue = () => { 62 | const {delay: {display}} = this.props; 63 | this.setState(state => ({ 64 | ...state, 65 | ...this.queuedNotification, 66 | open: true, 67 | })); 68 | this.timeout = setTimeout(() => { 69 | this.setState(state => ({ 70 | ...state, 71 | open: false, 72 | })); 73 | }, display); 74 | }; 75 | 76 | addNotification = (key, text) => { 77 | const {open} = this.state; 78 | const {delay: {animation}} = this.props; 79 | copy(key); 80 | this.queuedNotification = { 81 | key, 82 | text, 83 | }; 84 | if (this.timeout) { 85 | clearTimeout(this.timeout); 86 | } 87 | if (open) { 88 | this.setState(state => ({ 89 | ...state, 90 | open: false, 91 | })); 92 | this.queueTimeout = setTimeout(() => { 93 | this.queueTimeout = undefined; 94 | this.processNotificationQueue(); 95 | }, animation); 96 | } 97 | else if (!this.queueTimeout) { // we click when the notification is closing (open = false but animation still running) 98 | this.processNotificationQueue(); 99 | } 100 | }; 101 | 102 | snackbar = () => { 103 | const {open} = this.state; 104 | const {delay: {animation}} = this.props; 105 | return css` 106 | position: fixed; 107 | left: 25px; 108 | bottom: ${open ? '25px' : '-80px'}; 109 | opacity: ${open ? 1 : 0}; 110 | transition: all ${animation}ms linear; 111 | `; 112 | }; 113 | 114 | render() { 115 | const { 116 | text, 117 | key, 118 | } = this.state; 119 | const {clipboardContentCSS, snackbarContentCSS} = this.props; 120 | 121 | return ( 122 | 123 | 124 | 125 |
129 | 133 | 134 |
135 | 136 |

137 | {text} 138 |

139 |
140 |
141 | )} 142 | /> 143 | 144 |
145 | 146 | ); 147 | } 148 | } 149 | CopyNotification.defaultProps = { 150 | delay: { 151 | animation: 150, 152 | display: 2500, 153 | }, 154 | clipboardContentCSS, 155 | snackbarContentCSS, 156 | }; 157 | 158 | CopyNotification.propTypes = { 159 | delay: PropTypes.objectOf(PropTypes.number), 160 | clipboardContentCSS: PropTypes.string, 161 | snackbarContentCSS: PropTypes.string, 162 | }; 163 | 164 | return CopyNotification; 165 | }; 166 | 167 | export default withAddNotification; 168 | -------------------------------------------------------------------------------- /src/components/copyNotification/copyNotification.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {storiesOf} from '@storybook/react'; 3 | import {withKnobs, color} from '@storybook/addon-knobs'; 4 | import PropTypes from '../../utils/propTypes'; 5 | import withAddNotification from './copyNotification'; 6 | import Check from '../../icons/check'; 7 | import {darkSkyBlue} from '../../variables/colors'; 8 | 9 | storiesOf('CopyNotification', module) 10 | .addDecorator(withKnobs) 11 | .add('default', () => { 12 | const addNotificationButton = ({addNotification}) => ( 13 | 16 | ); 17 | 18 | addNotificationButton.propTypes = { 19 | addNotification: PropTypes.func.isRequired, 20 | }; 21 | 22 | const Button = withAddNotification(addNotificationButton); 23 | return 30 | ); 31 | 32 | addNotificationButton.propTypes = { 33 | addNotification: PropTypes.func.isRequired, 34 | }; 35 | 36 | const Button = withAddNotification(addNotificationButton); 37 | const delay = {animation: 300, display: 1000}; 38 | return 46 | ); 47 | 48 | addNotificationButton.propTypes = { 49 | addNotification: PropTypes.func.isRequired, 50 | }; 51 | 52 | const OwkestraCheck = () => ; 53 | 54 | const Button = withAddNotification(addNotificationButton, OwkestraCheck); 55 | return 17 | ); 18 | 19 | addNotificationButton.propTypes = { 20 | addNotification: PropTypes.func.isRequired, 21 | }; 22 | 23 | const Main = withAddNotification(addNotificationButton); 24 | 25 | test('The notification appears', () => { 26 | const {getByTestId} = render(
); 27 | 28 | expect(getByTestId('notification')).toHaveStyle('bottom: -80px'); 29 | fireEvent.click(getByTestId('button')); 30 | expect(getByTestId('notification')).toHaveStyle('bottom: 25px'); 31 | }); 32 | 33 | test('Key copied to the clipboard', () => { 34 | copy.mockReset(); 35 | const {getByTestId} = render(
); 36 | 37 | expect(copy).not.toHaveBeenCalled(); 38 | fireEvent.click(getByTestId('button')); 39 | expect(copy).toHaveBeenCalledTimes(1); 40 | expect(copy).toHaveBeenCalledWith('key'); 41 | }); 42 | -------------------------------------------------------------------------------- /src/components/copyNotification/snackbarContent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {css} from 'emotion'; 4 | import {white} from '../../variables/colors'; 5 | import {fontNormal} from '../../variables/font'; 6 | 7 | const paper = css` 8 | display: flex; 9 | align-items: center; 10 | flex-wrap: wrap; 11 | padding: 6px 24px; 12 | min-width: 288px; 13 | max-width: 568px; 14 | border-radius: 5px; 15 | box-shadow: 0 4px 8px 1px #bdbdbd; 16 | background-color: ${white}; 17 | `; 18 | 19 | const messagePadding = css` 20 | padding: 8px 0; 21 | font-size: ${fontNormal}; 22 | line-height: 16px; 23 | `; 24 | 25 | function SnackbarContent(props) { 26 | const { 27 | message, 28 | className, 29 | ...other 30 | } = props; 31 | return ( 32 |
36 |
37 | {message} 38 |
39 |
40 | ); 41 | } 42 | 43 | SnackbarContent.propTypes = { 44 | /** 45 | * The CSS class name of the wrapper element. 46 | */ 47 | className: PropTypes.string, 48 | /** 49 | * The message to display. 50 | */ 51 | message: PropTypes.node, 52 | key: PropTypes.string, 53 | }; 54 | 55 | SnackbarContent.defaultProps = { 56 | className: '', 57 | message: null, 58 | key: '', 59 | }; 60 | 61 | export default SnackbarContent; 62 | -------------------------------------------------------------------------------- /src/components/iconButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {css} from 'emotion'; 3 | import PropTypes from '../utils/propTypes'; 4 | import {ice} from '../variables/colors'; 5 | 6 | const roundButton = css` 7 | display: inline-flex; 8 | align-items: center; 9 | justify-content: center; 10 | width: 30px; 11 | height: 30px; 12 | border-radius: 15px; 13 | border: 1px solid ${ice}; 14 | padding: 0; 15 | background: none; 16 | cursor: pointer; 17 | 18 | &:hover { 19 | background-color: ${ice}; 20 | transition: background-color 200ms ease-out; 21 | } 22 | `; 23 | 24 | export const RoundButton = ({className, ...props}) => ( 25 | 47 | ); 48 | 49 | RoundedButton.propTypes = { 50 | disabled: PropTypes.bool, 51 | iconColor: PropTypes.string, 52 | iconColorDisabled: PropTypes.string, 53 | Icon: PropTypes.component, 54 | children: PropTypes.node, 55 | Button: PropTypes.component, 56 | }; 57 | 58 | RoundedButton.defaultProps = { 59 | disabled: false, 60 | iconColor: slate, 61 | iconColorDisabled: blueGrey, 62 | Icon: null, 63 | children: null, 64 | Button, 65 | }; 66 | -------------------------------------------------------------------------------- /src/components/roundedButton.stories.js: -------------------------------------------------------------------------------- 1 | import React, {Fragment} from 'react'; 2 | import {storiesOf} from '@storybook/react'; 3 | import {action} from '@storybook/addon-actions'; 4 | 5 | import {RoundedButton} from './roundedButton'; 6 | import Book from '../icons/book'; 7 | 8 | const onClick = action('onClick'); 9 | 10 | storiesOf('RoundedButton', module) 11 | .add('default', () => ( 12 | 13 | 14 | Text only 15 | 16 | 17 | Icon + text 18 | 19 | 20 | )) 21 | .add('disabled', () => ( 22 | 23 | 24 | Text only 25 | 26 | 27 | Icon + text 28 | 29 | 30 | )) 31 | .add('icon colors', () => ( 32 | 33 | 39 | Normal 40 | 41 | 48 | Disabled 49 | 50 | 51 | )); 52 | -------------------------------------------------------------------------------- /src/components/roundedButton.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, fireEvent} from '@testing-library/react'; 3 | import {RoundedButton} from './roundedButton'; 4 | 5 | test('It should handle onClick', () => { 6 | const callback = jest.fn(); 7 | const {container} = render(); 8 | const button = container.querySelector('button'); 9 | fireEvent.click(button); 10 | expect(callback).toHaveBeenCalled(); 11 | }); 12 | 13 | test('It should have a disabled state', () => { 14 | const callback = jest.fn(); 15 | const {container} = render(); 16 | const button = container.querySelector('button'); 17 | expect(button.disabled).toBeTruthy(); 18 | fireEvent.click(button); 19 | expect(callback).not.toHaveBeenCalled(); 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/searchBar/overrides/input.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import styled from '@emotion/styled'; 3 | import {css} from 'emotion'; 4 | import {noop} from 'lodash'; 5 | 6 | import PropTypes from '../../../utils/propTypes'; 7 | 8 | import {match, parse} from './utils/autosuggest-highlight'; 9 | 10 | import {ice} from '../../../variables/colors'; 11 | import {fontNormal} from '../../../variables/font'; 12 | 13 | const Logic = styled('span')` 14 | color: #1935a7; 15 | margin: 0 auto; 16 | font-weight: bold; 17 | `; 18 | 19 | const placeholder = ` 20 | color: darkgray; 21 | opacity: 0.7; 22 | `; 23 | 24 | const styles = { 25 | /* Styles applied to the root element. */ 26 | root: disabled => css` 27 | // Mimics the default input display property used by browsers for an input. 28 | display: block; 29 | position: relative; 30 | cursor: text; 31 | font-family: 'Lato'; 32 | font-size: ${fontNormal}; 33 | line-height: 1.1875em; // Reset (19px), match the native input line-height 34 | ${disabled ? ` 35 | color: ${ice}; 36 | ` : ''} 37 | `, 38 | /* Styles applied to the root element if `fullWidth={true}`. */ 39 | fullWidth: css` 40 | width: 100%; 41 | `, 42 | /* Styles applied to the `input` Wrapper element. */ 43 | inputWrapper: css` 44 | width: 200px; 45 | display: inline-block; 46 | `, 47 | /* Styles applied to the `input` element. */ 48 | input: disabled => css` 49 | font: inherit; 50 | color: currentColor; 51 | padding: 0; 52 | border: 0; 53 | box-sizing: content-box; 54 | vertical-align: middle; 55 | background: none; 56 | margin: 0; // Reset for Safari 57 | // Remove grey highlight 58 | -webkit-tap-highlight-color: transparent; 59 | display: block; 60 | // Make the flex item shrink with Firefox 61 | min-width: 0; 62 | flex-grow: 1; 63 | &::-webkit-input-placeholder {${placeholder}}; 64 | &::-moz-placeholder {${placeholder}}; // Firefox 19+ 65 | &:-ms-input-placeholder {${placeholder}}; // IE 11 66 | &::-ms-input-placeholder {${placeholder}}; // Edge 67 | &:focus { 68 | outline: 0; 69 | } 70 | // Reset Firefox invalid required input style 71 | &:invalid { 72 | box-shadow: none; 73 | } 74 | &::-webkit-search-decoration { 75 | // Remove the padding when type=search. 76 | -webkit-appearance: none; 77 | }, 78 | 79 | ${disabled ? 'opacity: 1;' : ''} // Reset iOS opacity 80 | `, 81 | /* Styles applied to the `input` element if `margin="dense"`. */ 82 | inputMarginDense: css` 83 | padding-top: 3px; 84 | `, 85 | /* Styles applied to the `input` element if `type` is not "text"`. */ 86 | inputType: css` 87 | // type="date" or type="time", etc. have specific styles we need to reset. 88 | height: 1.1875em; // Reset (19px), match the native input line-height 89 | `, 90 | /* Styles applied to the `input` element if `type="search"`. */ 91 | inputTypeSearch: css` 92 | // Improve type search style. 93 | -moz-appearance: textfield; 94 | -webkit-appearance: textfield; 95 | `, 96 | paper: css` 97 | color: black; 98 | background-color: white; 99 | box-shadow: 0px 1px 2px 0px darkgray; 100 | `, 101 | popperOpen: css` 102 | z-index: 2; 103 | position: absolute; 104 | `, 105 | popperClose: css` 106 | display: none; 107 | `, 108 | highligthed: css` 109 | font-weight: bold; 110 | `, 111 | }; 112 | 113 | const menuItemDefaultCss = ` 114 | line-height: 50px; 115 | padding-right: 10px; 116 | padding-left: 10px; 117 | min-width: 100px; 118 | cursor: pointer; 119 | &:hover { 120 | background-color: #dfdfdf; 121 | }; 122 | `; 123 | 124 | class Input extends Component { 125 | input = null; // Holds the input reference 126 | 127 | handleFocus = event => { 128 | // Fix a bug with IE11 where the focus/blur events are triggered 129 | // while the input is disabled. 130 | 131 | if (this.props.onFocus) { 132 | this.props.onFocus(event); 133 | } 134 | }; 135 | 136 | handleBlur = event => { 137 | if (this.props.onBlur) { 138 | this.props.onBlur(event); 139 | } 140 | }; 141 | 142 | handleChange = event => { 143 | // Perform in the willUpdate 144 | if (this.props.onChange) { 145 | this.props.onChange(event); 146 | } 147 | }; 148 | 149 | handleRefInput = ref => { 150 | this.inputRef = ref; 151 | let refProp; 152 | 153 | if (this.props.inputRef) { 154 | refProp = this.props.inputRef; 155 | } 156 | else if (this.props.inputProps && this.props.inputProps.ref) { 157 | refProp = this.props.inputProps.ref; 158 | } 159 | 160 | if (refProp) { 161 | if (typeof refProp === 'function') { 162 | refProp(ref); 163 | } 164 | else { 165 | refProp.current = ref; 166 | } 167 | } 168 | }; 169 | 170 | menuItem = isLogic => isLogic ? css` 171 | ${menuItemDefaultCss}; 172 | text-align: center; 173 | ` : css`${menuItemDefaultCss}` 174 | ; 175 | 176 | menuItemHighlighted = isLogic => isLogic ? css` 177 | ${menuItemDefaultCss} 178 | text-align: center; 179 | background-color: #dfdfdf; 180 | ` : css` 181 | ${menuItemDefaultCss}; 182 | background-color: #dfdfdf; 183 | `; 184 | 185 | render() { 186 | const { 187 | className: classNameProp, // eslint-disable-line no-unused-vars 188 | classes, 189 | endAdornment, 190 | inputComponent, 191 | inputProps: {className: inputPropsClassName, ...inputPropsProp} = {}, // eslint-disable-line no-unused-vars 192 | onKeyDown, 193 | placeholder, 194 | startAdornment, 195 | value, 196 | isOpen, 197 | getItemProps, 198 | inputValue, 199 | highlightedIndex, 200 | suggestions, 201 | inputRef, // eslint-disable-line no-unused-vars 202 | inputWrapperRef, // eslint-disable-line no-unused-vars 203 | ...other 204 | } = this.props; 205 | 206 | const className = css` 207 | ${styles.formControl}; 208 | `; 209 | const inputClassName = css` 210 | ${styles.input()}; 211 | ${classes.input} 212 | `; 213 | 214 | const inputwrapperClassName = css` 215 | ${styles.inputWrapper}; 216 | ${classes.inputWrapper} 217 | `; 218 | 219 | const InputComponent = 'input'; 220 | let inputProps = { 221 | ...inputPropsProp, 222 | ref: this.handleRefInput, 223 | }; 224 | 225 | if (inputComponent) { 226 | inputProps = { 227 | // Rename ref to inputRef as we don't know the 228 | // provided `inputComponent` structure. 229 | inputRef: this.handleRefInput, 230 | ...inputProps, 231 | ref: null, 232 | }; 233 | } 234 | 235 | return ( 236 |
237 | {startAdornment} 238 |
239 | 250 |
254 |
255 | {suggestions(inputValue).map((suggestion, index) => { 256 | const isHighlighted = highlightedIndex === index; 257 | const itemProps = getItemProps({item: suggestion}); 258 | 259 | const highlighted = parse(suggestion.label, match(suggestion.label, inputValue, {insideWords: true})); 260 | 261 | return ( 262 |
267 | { 268 | suggestion.isLogic 269 | ? ( 270 | 271 | {suggestion.label} 272 | 273 | ) 274 | 275 | : highlighted.map((o, i) => o.highlight 276 | ? ( 277 | 278 | {decodeURIComponent(o.text)} 279 | 280 | ) 281 | : decodeURIComponent(o.text)) 282 | } 283 |
284 | ); 285 | })} 286 |
287 |
288 |
289 | {endAdornment} 290 |
291 | ); 292 | } 293 | } 294 | 295 | Input.propTypes = { 296 | /** 297 | * Override or extend the styles applied to the component. 298 | * See [CSS API](#css-api) below for more details. 299 | */ 300 | classes: PropTypes.shape({ 301 | input: PropTypes.string, 302 | inputWrapper: PropTypes.string, 303 | }).isRequired, 304 | /** 305 | * The CSS class name of the wrapper element. 306 | */ 307 | className: PropTypes.string, 308 | /** 309 | * End ` 310 | InputAdornment` for this component. 311 | */ 312 | endAdornment: PropTypes.node, 313 | /** 314 | * The component used for the native input. 315 | * Either a string to use a DOM element or a component. 316 | */ 317 | inputComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.shape({})]), 318 | /** 319 | * Attributes applied to the ` 320 | input` element. 321 | */ 322 | inputProps: PropTypes.shape({ 323 | ref: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({})]), 324 | }), 325 | /** 326 | * Use that property to pass a ref callback to the native input component. 327 | */ 328 | inputRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({})]), 329 | /** 330 | * Use that property to pass a ref callback to wrapper of the native input component. 331 | */ 332 | inputWrapperRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({})]), 333 | /** 334 | * If ` 335 | dense`, will adjust vertical spacing. This is normally obtained via context from 336 | * FormControl. 337 | */ 338 | margin: PropTypes.oneOf(['dense', 'none']), 339 | /** 340 | * @ignore 341 | */ 342 | onBlur: PropTypes.func, 343 | /** 344 | * Callback fired when the value is changed. 345 | * 346 | * @param {object} event The event source of the callback. 347 | * You can pull out the new value by accessing ` 348 | event.target.value`. 349 | */ 350 | onChange: PropTypes.func, 351 | /** 352 | * @ignore 353 | */ 354 | onFocus: PropTypes.func, 355 | /** 356 | * @ignore 357 | */ 358 | onKeyDown: PropTypes.func, 359 | /** 360 | * The short hint displayed in the input before the user enters a value. 361 | */ 362 | placeholder: PropTypes.string, 363 | /** 364 | * Start ` 365 | InputAdornment` for this component. 366 | */ 367 | startAdornment: PropTypes.node, 368 | /** 369 | * The input value, required for a controlled component. 370 | */ 371 | // eslint-disable-next-line react/require-default-props 372 | value: PropTypes.oneOfType([ 373 | PropTypes.string, 374 | PropTypes.number, 375 | PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), 376 | ]), 377 | /** 378 | * Display the dropdown list 379 | */ 380 | isOpen: PropTypes.bool, 381 | /** 382 | * Get item props for the dropdown list menu itens 383 | */ 384 | getItemProps: PropTypes.func, 385 | /** 386 | * The input value given from the user for filtering on suggestions 387 | */ 388 | inputValue: PropTypes.string, 389 | /** 390 | * The index to know which to highlight 391 | */ 392 | highlightedIndex: PropTypes.number, 393 | /** 394 | * Function for getting suggestions 395 | */ 396 | suggestions: PropTypes.func, 397 | }; 398 | 399 | Input.defaultProps = { 400 | className: '', 401 | endAdornment: '', 402 | inputComponent: '', 403 | inputProps: {}, 404 | inputRef: noop, 405 | inputWrapperRef: noop, 406 | margin: 'none', 407 | onBlur: noop, 408 | onChange: noop, 409 | onFocus: noop, 410 | onKeyDown: noop, 411 | placeholder: '', 412 | startAdornment: '', 413 | isOpen: false, 414 | getItemProps: noop, 415 | inputValue: '', 416 | highlightedIndex: 0, 417 | suggestions: noop, 418 | }; 419 | 420 | export default Input; 421 | -------------------------------------------------------------------------------- /src/components/searchBar/overrides/searchInput.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {css} from 'emotion'; 3 | import {noop} from 'lodash'; 4 | 5 | import { 6 | ChipActions, 7 | ChipButton, 8 | ChipTitle, 9 | ChipWrapper, 10 | } from '../../chip'; 11 | import PropTypes from '../../../utils/propTypes'; 12 | import {spacingExtraSmall, spacingSmall} from '../../../variables/spacing'; 13 | import {fontLarge} from '../../../variables/font'; 14 | 15 | // modified textField for our needs 16 | import TextField from './textField'; 17 | import Clear from '../../../icons/clear'; 18 | 19 | const parentChip = isLogic => css` 20 | padding: ${spacingExtraSmall} ${spacingSmall}; 21 | display: inline-flex; 22 | font-size: ${fontLarge}; 23 | ${isLogic ? 'color: #1935a7;font-weight: bold' : 'inherit'}; 24 | `; 25 | 26 | const styles = placeholder => ({ 27 | inputWrapper: css` 28 | margin: 2px 8px; 29 | font-size: ${fontLarge}; 30 | width: 80px; 31 | flex-grow: 1; 32 | `, 33 | input: css` 34 | ${placeholder ? '' : 'width: 400px;'} // needed to display the entire placeholder 35 | font-size: ${fontLarge}; 36 | height: 30px; 37 | `, 38 | }); 39 | 40 | class SearchInput extends Component { 41 | handleClick = () => { 42 | const {openMenu, clickInput} = this.props; 43 | openMenu(); 44 | clickInput(); 45 | }; 46 | 47 | inputProps = () => { 48 | const { 49 | selectedItem, isOpen, inputValue, highlightedIndex, 50 | getInputProps, handleInputChange, handleKeyDown, handleDelete, 51 | getItemProps, placeholder, 52 | } = this.props; 53 | 54 | return getInputProps({ 55 | startAdornment: selectedItem.map(o => o.child 56 | ? ( 57 | 58 | {`${o.parent}:${decodeURIComponent(o.child)}`} 59 | 60 | 61 | 62 | 63 | ) 64 | : ( 65 | 66 | {`${o.parent}${o.isLogic ? '' : ':'}`} 67 | 68 | )), 69 | onChange: handleInputChange, 70 | onKeyDown: handleKeyDown, 71 | placeholder: selectedItem.length ? '' : placeholder, 72 | classes: styles(selectedItem.length), 73 | isOpen, 74 | getItemProps, 75 | inputValue, 76 | highlightedIndex, 77 | suggestions: this.getSuggestions, 78 | }); 79 | }; 80 | 81 | getSuggestions = inputValue => { 82 | const {suggestions} = this.props; 83 | 84 | return suggestions.filter(suggestion => (!inputValue || suggestion.label.toLowerCase().includes(inputValue.replace(/ /g, '').toLowerCase()))); 85 | }; 86 | 87 | render() { 88 | const {input} = this.props; 89 | 90 | return ( 91 |
92 | 96 |
97 | ); 98 | } 99 | } 100 | 101 | SearchInput.defaultProps = { 102 | input: null, 103 | clickInput: noop, 104 | openMenu: noop, 105 | suggestions: [], 106 | selectedItem: [], 107 | getInputProps: noop, 108 | handleInputChange: noop, 109 | handleKeyDown: noop, 110 | handleDelete: noop, 111 | getItemProps: noop, 112 | isOpen: false, 113 | inputValue: '', 114 | highlightedIndex: 0, 115 | placeholder: '', 116 | }; 117 | 118 | SearchInput.propTypes = { 119 | input: PropTypes.shape({}), 120 | clickInput: PropTypes.func, 121 | openMenu: PropTypes.func, 122 | suggestions: PropTypes.arrayOf(PropTypes.shape({})), 123 | selectedItem: PropTypes.arrayOf(PropTypes.shape({})), 124 | getInputProps: PropTypes.func, 125 | handleInputChange: PropTypes.func, 126 | handleKeyDown: PropTypes.func, 127 | handleDelete: PropTypes.func, 128 | getItemProps: PropTypes.func, 129 | isOpen: PropTypes.bool, 130 | inputValue: PropTypes.string, 131 | highlightedIndex: PropTypes.number, 132 | placeholder: PropTypes.string, 133 | }; 134 | 135 | export default SearchInput; 136 | -------------------------------------------------------------------------------- /src/components/searchBar/overrides/textField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {noop} from 'lodash'; 3 | import PropTypes from '../../../utils/propTypes'; 4 | import Input from './input'; 5 | 6 | function TextField(props) { 7 | const { 8 | className, 9 | defaultValue, 10 | InputProps, 11 | inputRef, 12 | onBlur, 13 | onChange, 14 | onFocus, 15 | placeholder, 16 | value, 17 | ...other 18 | } = props; 19 | 20 | const InputElement = ( 21 | 31 | ); 32 | 33 | return ( 34 |
38 | {InputElement} 39 |
40 | ); 41 | } 42 | 43 | TextField.propTypes = { 44 | /** 45 | * @ignore 46 | */ 47 | className: PropTypes.string, 48 | /** 49 | * The default value of the `Input` element. 50 | */ 51 | // eslint-disable-next-line react/require-default-props 52 | defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 53 | /** 54 | * Properties applied to the `Input` element. 55 | */ 56 | InputProps: PropTypes.shape({}), 57 | /** 58 | * Use that property to pass a ref callback to the native input component. 59 | */ 60 | inputRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({})]), 61 | /** 62 | * @ignore 63 | */ 64 | onBlur: PropTypes.func, 65 | /** 66 | * Callback fired when the value is changed. 67 | * 68 | * @param {object} event The event source of the callback. 69 | * You can pull out the new value by accessing `event.target.value`. 70 | */ 71 | onChange: PropTypes.func, 72 | /** 73 | * @ignore 74 | */ 75 | onFocus: PropTypes.func, 76 | /** 77 | * The short hint displayed in the input before the user enters a value. 78 | */ 79 | placeholder: PropTypes.string, 80 | /** 81 | * The value of the `Input` element, required for a controlled component. 82 | */ 83 | // eslint-disable-next-line react/require-default-props 84 | value: PropTypes.oneOfType([ 85 | PropTypes.string, 86 | PropTypes.number, 87 | PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), 88 | ]), 89 | }; 90 | 91 | TextField.defaultProps = { 92 | className: '', 93 | InputProps: {}, 94 | inputRef: {}, 95 | onBlur: noop, 96 | onChange: noop, 97 | onFocus: noop, 98 | placeholder: '', 99 | }; 100 | 101 | export default TextField; 102 | -------------------------------------------------------------------------------- /src/components/searchBar/overrides/utils/autosuggest-highlight.js: -------------------------------------------------------------------------------- 1 | import {clean as removeDiacritics} from 'diacritic'; 2 | 3 | // https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions#Using_special_characters 4 | const specialCharsRegex = /[.*+?^${}()|[\]\\]/g; 5 | 6 | // http://www.ecma-international.org/ecma-262/5.1/#sec-15.10.2.6 7 | const wordCharacterRegex = /[a-z0-9_]/i; 8 | 9 | const whitespacesRegex = /\s+/; 10 | 11 | function escapeRegexCharacters(str) { 12 | return str.replace(specialCharsRegex, '\\$&'); 13 | } 14 | 15 | export const match = (t, q, o) => { 16 | const options = { 17 | insideWords: false, 18 | findAllOccurrences: false, 19 | requireMatchAll: false, 20 | ...o, 21 | }; 22 | 23 | let text = removeDiacritics(t); 24 | const query = removeDiacritics(q); 25 | 26 | return ( 27 | query 28 | .trim() 29 | .split(whitespacesRegex) 30 | // If query is blank, we'll get empty string here, so let's filter it out. 31 | .filter(word => word.length > 0) 32 | .reduce((result, word) => { 33 | const wordLen = word.length; 34 | const prefix = !options.insideWords && wordCharacterRegex.test(word[0]) ? '\\b' : ''; 35 | const regex = new RegExp(prefix + escapeRegexCharacters(word), 'i'); 36 | let occurrence; 37 | let index; 38 | 39 | occurrence = regex.exec(text); 40 | if (options.requireMatchAll && occurrence === null) { 41 | text = ''; 42 | return []; 43 | } 44 | 45 | while (occurrence) { 46 | index = occurrence.index; 47 | result.push([index, index + wordLen]); 48 | 49 | // Replace what we just found with spaces so we don't find it again. 50 | text = text.slice(0, index) 51 | + new Array(wordLen + 1).join(' ') 52 | + text.slice(index + wordLen); 53 | 54 | if (!options.findAllOccurrences) { 55 | break; 56 | } 57 | 58 | occurrence = regex.exec(text); 59 | } 60 | 61 | return result; 62 | }, []) 63 | .sort((match1, match2) => match1[0] - match2[0]) 64 | ); 65 | }; 66 | 67 | export const parse = (t, matches) => { 68 | const result = []; 69 | 70 | const text = t; 71 | 72 | if (matches.length === 0) { 73 | result.push({ 74 | text, 75 | highlight: false, 76 | }); 77 | } 78 | else if (matches[0][0] > 0) { 79 | result.push({ 80 | text: text.slice(0, matches[0][0]), 81 | highlight: false, 82 | }); 83 | } 84 | 85 | matches.forEach((match, i) => { 86 | const startIndex = match[0]; 87 | const endIndex = match[1]; 88 | 89 | result.push({ 90 | text: text.slice(startIndex, endIndex), 91 | highlight: true, 92 | }); 93 | 94 | if (i === matches.length - 1) { 95 | if (endIndex < text.length) { 96 | result.push({ 97 | text: text.slice(endIndex, text.length), 98 | highlight: false, 99 | }); 100 | } 101 | } 102 | else if (endIndex < matches[i + 1][0]) { 103 | result.push({ 104 | text: text.slice(endIndex, matches[i + 1][0]), 105 | highlight: false, 106 | }); 107 | } 108 | }); 109 | 110 | return result; 111 | }; 112 | -------------------------------------------------------------------------------- /src/components/searchBar/searchBar.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import keycode from 'keycode'; 3 | import uuidv4 from 'uuid/v4'; 4 | import decodeUriComponent from 'decode-uri-component'; 5 | import {isEqual, noop} from 'lodash'; 6 | import styled from '@emotion/styled'; 7 | import Downshift from 'downshift'; 8 | import {css} from 'emotion'; 9 | 10 | import ClearIcon from '../../icons/clear'; 11 | import PropTypes from '../../utils/propTypes'; 12 | 13 | import {IconButton} from '../iconButton'; 14 | import SearchInput from './overrides/searchInput'; 15 | 16 | import {ice, darkSkyBlue, white} from '../../variables/colors'; 17 | 18 | const InputWrapper = styled('div')` 19 | border: 1px solid ${ice}; 20 | background-color: ${white}; 21 | display: flex; 22 | align-items: center; 23 | justify-content: space-between; 24 | min-height: 40px; 25 | border-radius: 20px; 26 | padding: 1px; 27 | position: static; 28 | `; 29 | 30 | const searchInputWrapper = css` 31 | flex-grow: 1; 32 | margin-left: 2px; 33 | `; 34 | 35 | const clearButton = css` 36 | border: none; 37 | z-index: 1; 38 | outline: none; 39 | margin-right: 2px; 40 | 41 | &:focus { 42 | box-shadow: 0 0 3pt 3pt ${darkSkyBlue}; 43 | } 44 | `; 45 | 46 | // use getRootProps https://github.com/paypal/downshift#getrootprops 47 | const SearchInputWrapper = ({innerRef, ...rest}) => ( 48 |
53 | ); 54 | 55 | SearchInputWrapper.propTypes = { 56 | innerRef: PropTypes.func.isRequired, 57 | }; 58 | 59 | class SearchBar extends Component { 60 | constructor(props) { 61 | super(props); 62 | 63 | this.input = React.createRef(); 64 | } 65 | 66 | componentDidMount() { 67 | // init suggestions 68 | const {location, setState, selectedItem} = this.props; 69 | 70 | let newSelectedItem = []; 71 | // fill search from state.location 72 | if (location && location.query && location.query.search) { 73 | // get groups separated by -OR- 74 | const groups = location.query.search.split('-OR-'); 75 | 76 | newSelectedItem = groups.reduce((p, group) => { 77 | // create related chips 78 | const chips = group.split(',').map(o => { 79 | const el = decodeUriComponent(o).split(':'); 80 | 81 | return { 82 | parent: el[0], 83 | child: el.splice(1).join(':'), 84 | uuid: uuidv4(), 85 | isLogic: false, 86 | }; 87 | }); 88 | 89 | // do not hoist it, uuid need to be different 90 | const logic = { 91 | parent: '-OR-', 92 | child: '', 93 | uuid: uuidv4(), 94 | isLogic: true, 95 | }; 96 | 97 | return [ 98 | ...p, 99 | ...(group ? chips : []), 100 | logic, // will add an extra logic el on the last iteration 101 | ]; 102 | }, []).slice(0, -1); // remove last added `-OR-` 103 | 104 | if (!isEqual(selectedItem, newSelectedItem)) { 105 | setState({ 106 | selectedItem: newSelectedItem, 107 | toUpdate: false, 108 | }); 109 | } 110 | } 111 | } 112 | 113 | handleKeyDown = event => { 114 | const {setState, inputValue, selectedItem} = this.props; 115 | 116 | if (selectedItem.length && !inputValue.length && keycode(event) === 'backspace') { 117 | const l = selectedItem.length; 118 | 119 | if (l) { 120 | const newSelectedItems = selectedItem.slice(0, l - 1); 121 | const last = selectedItem[l - 1]; 122 | 123 | setState({ 124 | selectedItem: newSelectedItems, 125 | isParent: true, 126 | item: '', 127 | toUpdate: !(last.isLogic || !last.child), 128 | }); 129 | } 130 | } 131 | }; 132 | 133 | handleInputChange = event => { 134 | const {setState} = this.props; 135 | 136 | setState({ 137 | inputValue: event.target.value, 138 | toUpdate: false, 139 | }); 140 | }; 141 | 142 | handleChange = item => { 143 | // item may be null when handleChange is triggered by hitting the ESC key 144 | if (item) { 145 | const { 146 | parentSuggestions, isParent, selectedItem, 147 | setState, 148 | } = this.props; 149 | 150 | let newSelectedItem, 151 | toUpdate = false; 152 | 153 | if (!isParent) { // remove precedent parent and add child 154 | const prev = selectedItem.pop(); 155 | newSelectedItem = [...selectedItem, {...prev, child: item.label}]; 156 | toUpdate = true; 157 | } 158 | // if is parent, simply add 159 | else { 160 | newSelectedItem = [ 161 | ...selectedItem, { 162 | parent: item.label, 163 | child: '', 164 | uuid: uuidv4(), 165 | isLogic: item.isLogic, 166 | }]; 167 | } 168 | 169 | // calculate if previous in parent menu 170 | const selectedParent = parentSuggestions.map(o => o.label).includes(item.label); 171 | 172 | // set item in redux, and launch related sagas for fetching list if needed 173 | setState({ 174 | isParent: item.isLogic || !selectedParent, 175 | inputValue: '', 176 | selectedItem: newSelectedItem, 177 | item: selectedParent || item.isLogic ? item.label : '', 178 | toUpdate, 179 | }); 180 | } 181 | }; 182 | 183 | handleDelete = item => () => { 184 | const {setState, selectedItem} = this.props; 185 | 186 | // remove empty parent, the item we want to delete 187 | let newSelectedItems = selectedItem.filter(o => !( 188 | (o.child === '' && !o.isLogic) // remove parent without child 189 | || o.uuid === item.uuid), // remove item clicked 190 | 191 | ); 192 | 193 | // remove -OR- item if not a chip before (i.e nothing or another -OR-) 194 | newSelectedItems = newSelectedItems.filter((o, i) => !( 195 | o.isLogic && i === 0 // remove first item if isLogic 196 | || o.isLogic && i > 0 && newSelectedItems[i - 1].isLogic), // remove isLogic if precedent isLogic 197 | 198 | ); 199 | 200 | // need to setItem correctly after deleting 201 | const l = newSelectedItems.length, 202 | last = l ? newSelectedItems[l - 1] : undefined; 203 | 204 | setState({ 205 | selectedItem: newSelectedItems, 206 | isParent: true, 207 | item: last && last.isLogic ? '-OR-' : '', 208 | toUpdate: true, 209 | }); 210 | 211 | this.clickInput(); 212 | }; 213 | 214 | handleOuterClick = () => { 215 | const {setState, inputValue} = this.props; 216 | 217 | if (inputValue) { 218 | setState({ 219 | inputValue: '', 220 | }); 221 | } 222 | }; 223 | 224 | clear = () => { 225 | const {setState} = this.props; 226 | 227 | setState({ 228 | selectedItem: [], 229 | isParent: true, 230 | item: '', 231 | toUpdate: true, 232 | }); 233 | 234 | this.clickInput(); 235 | }; 236 | 237 | clickInput = () => { 238 | setTimeout(() => { 239 | // should appear after blur of input (need to call setTimeout as downshift does) 240 | // stay on focus 241 | if (this.input.current) { 242 | this.input.current.focus(); 243 | } 244 | }, 0); 245 | }; 246 | 247 | searchInput = props => { 248 | const {suggestions, placeholder} = this.props; 249 | 250 | // due to ugly render props philosophy of Downshift, we have to extract the declaration in a function... 251 | return ( 252 | 253 | 263 | 264 | ); 265 | }; 266 | 267 | itemToString = item => item === null ? '' : item.label; 268 | 269 | render() { 270 | const {inputValue, selectedItem} = this.props; 271 | 272 | return ( 273 | 274 | 286 | {this.searchInput} 287 | 288 | 289 | 296 | 297 | ); 298 | } 299 | } 300 | 301 | SearchBar.propTypes = { 302 | location: PropTypes.shape({ 303 | query: PropTypes.shape({ 304 | search: PropTypes.string, 305 | }), 306 | }), 307 | suggestions: PropTypes.arrayOf(PropTypes.shape({})), 308 | parentSuggestions: PropTypes.arrayOf(PropTypes.shape({})), 309 | setState: PropTypes.func, 310 | inputValue: PropTypes.string, 311 | selectedItem: PropTypes.arrayOf(PropTypes.shape({ 312 | isLogic: PropTypes.bool, 313 | child: PropTypes.string, 314 | })), 315 | isParent: PropTypes.bool, 316 | placeholder: PropTypes.string, 317 | }; 318 | 319 | SearchBar.defaultProps = { 320 | location: null, 321 | suggestions: [], 322 | parentSuggestions: [], 323 | setState: noop, 324 | inputValue: '', 325 | selectedItem: [], 326 | isParent: true, 327 | placeholder: '', 328 | }; 329 | 330 | export default SearchBar; 331 | -------------------------------------------------------------------------------- /src/components/searchBar/searchBar.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {storiesOf} from '@storybook/react'; 3 | import {StateDecorator, Store} from '@sambego/storybook-state'; 4 | import { 5 | defaultState, 6 | getSuggestions, 7 | } from './searchBarJest'; 8 | import SearchBar from './searchBar'; 9 | 10 | const store = new Store(defaultState); 11 | 12 | let previousState = {...defaultState}; 13 | 14 | const setState = state => { 15 | store.set(state); 16 | }; 17 | 18 | setState({...defaultState, ...getSuggestions(defaultState.isParent, [])}); 19 | 20 | store.subscribe(state => { 21 | if (previousState.isParent !== state.isParent || state.item === '-OR-' || ( 22 | !state.selectedItem.length && state.suggestions[0].label === '-OR-' 23 | )) { 24 | previousState = {...state}; 25 | store.set({ 26 | ...state, 27 | ...getSuggestions(state.isParent, state.selectedItem, state.item), 28 | }); 29 | } 30 | previousState = {...state}; 31 | }); 32 | storiesOf('SearchBar', module) 33 | .addDecorator(StateDecorator(store)) 34 | .add('default', () => ( 35 | 44 | )); 45 | -------------------------------------------------------------------------------- /src/components/searchBar/searchBar.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, fireEvent} from '@testing-library/react'; 3 | import {createStore} from 'redux'; 4 | import {Provider, connect} from 'react-redux'; 5 | import { 6 | defaultState, getSuggestions, 7 | } from './searchBarJest'; 8 | 9 | import SearchBar from './searchBar'; 10 | 11 | const initialStateWithSelectedItem = { 12 | ...defaultState, 13 | selectedItem: [{parent: 'AAAA', child: '2', uuid: 'aee83084-fafb-4d26-8144-883c45244183'}], 14 | }; 15 | 16 | const initialStateWithLocation = { 17 | ...defaultState, 18 | location: {query: {search: 'test'}}, 19 | }; 20 | 21 | const initialStateWithSelectedItemAndLocation = { 22 | ...defaultState, 23 | selectedItem: [{parent: 'AAAA', child: '2', uuid: 'aee83084-fafb-4d26-8144-883c45244183'}], 24 | location: {query: {search: 'test'}}, 25 | }; 26 | 27 | const renderSearchBar = (initialState = defaultState) => { 28 | const reducer = (state = initialState, {type, payload}) => { 29 | switch (type) { 30 | case 'SET': 31 | return { 32 | ...state, 33 | ...getSuggestions(state.isParent, state.selectedItem, state.item), 34 | ...payload, 35 | }; 36 | default: 37 | return { 38 | ...state, 39 | ...getSuggestions(state.isParent, state.selectedItem, state.item), 40 | }; 41 | } 42 | }; 43 | const store = createStore(reducer, initialState); 44 | 45 | const mapStateToProps = ({ 46 | inputValue, suggestions, parentSuggestions, isParent, location, selectedItem, 47 | }) => ({ 48 | inputValue, 49 | suggestions, 50 | parentSuggestions, 51 | isParent, 52 | location, 53 | selectedItem, 54 | placeholder: 'Custom placeholder', 55 | }); 56 | 57 | const mapDispatchToProps = dispatch => ({ 58 | setState: payload => dispatch({type: 'SET', payload}), 59 | }); 60 | 61 | const ReduxSearchBar = connect(mapStateToProps, mapDispatchToProps)(SearchBar); 62 | return render(( 63 | 64 | 65 | 66 | 67 | )); 68 | }; 69 | 70 | test('The popper is displayed when the searchbar is clicked', () => { 71 | const {getByTestId} = renderSearchBar(); 72 | 73 | expect(getByTestId('popper')).toHaveStyle('display: none;'); 74 | fireEvent.click(getByTestId('searchbar')); 75 | expect(getByTestId('popper')).toHaveStyle('display: block;'); 76 | }); 77 | 78 | test('The width is 400px when there are no suggestions selected', () => { 79 | const {getByTestId} = renderSearchBar(); 80 | 81 | expect(getByTestId('searchbar')).toHaveStyle('width: 400px;'); 82 | }); 83 | 84 | test('The width isn\'t 400px when there are suggestions selected', () => { 85 | const {getByTestId} = renderSearchBar(initialStateWithSelectedItem); 86 | 87 | expect(getByTestId('searchbar')).not.toHaveStyle('width: 400px;'); 88 | }); 89 | 90 | test('The placeholder is displayed when there are no selected items', () => { 91 | const {getByTestId} = renderSearchBar(); 92 | 93 | expect(getByTestId('searchbar').placeholder).toEqual('Custom placeholder'); 94 | }); 95 | 96 | test('The placeholder isn\'t displayed when there are selected items', () => { 97 | const {getByTestId} = renderSearchBar(initialStateWithSelectedItem); 98 | 99 | expect(getByTestId('searchbar').placeholder).toEqual(''); 100 | }); 101 | 102 | test('The clear button remove every selected items', () => { 103 | const {getByTestId} = renderSearchBar(initialStateWithSelectedItem); 104 | 105 | expect(getByTestId('chip-aee83084-fafb-4d26-8144-883c45244183')).toBeDefined(); 106 | fireEvent.click(getByTestId('button')); 107 | expect(() => { 108 | getByTestId('chip-aee83084-fafb-4d26-8144-883c45244183'); 109 | }).toThrow(); 110 | }); 111 | 112 | test('Test if the location is synchronized', () => { 113 | const {getByText} = renderSearchBar(initialStateWithLocation); 114 | 115 | expect(getByText('test:')).toBeDefined(); 116 | }); 117 | 118 | test('Test if the location overrides selectedItem', () => { 119 | const {getByText} = renderSearchBar(initialStateWithSelectedItemAndLocation); 120 | 121 | expect(getByText('test:')).toBeDefined(); 122 | }); 123 | -------------------------------------------------------------------------------- /src/components/searchBar/searchBarJest.js: -------------------------------------------------------------------------------- 1 | import {uniqBy} from 'lodash'; 2 | 3 | export const defaultState = { 4 | inputValue: '', 5 | isParent: true, 6 | selectedItem: [], 7 | suggestions: [], 8 | parentSuggestions: [], 9 | }; 10 | 11 | export const suggestions = [ 12 | 'AAAA:1', 13 | 'AAAA:2', 14 | 'BBBB:1', 15 | 'BBBB:3', 16 | ]; 17 | 18 | export const getSuggestions = (isParent, selectedItem, item) => { 19 | const isLogic = item === '-OR-'; 20 | const level_1 = uniqBy(suggestions.map(s => ({label: s.split(':')[0]})), 'label'); 21 | 22 | if (isParent) { 23 | return { 24 | suggestions: [ 25 | ...(selectedItem.length && !isLogic ? [{label: '-OR-', isLogic: true}] : []), 26 | ...level_1, 27 | ], 28 | parentSuggestions: level_1, 29 | item: isLogic ? '' : item, 30 | }; 31 | } 32 | 33 | const s = suggestions.reduce((p, c) => { 34 | const [parent, child] = c.split(':'); 35 | return [ 36 | ...p, 37 | ...(parent === selectedItem[0].parent ? [{label: child}] : []), 38 | ]; 39 | }, []); 40 | const level_2 = uniqBy(s, 'label'); 41 | 42 | return { 43 | suggestions: level_2, 44 | parentSuggestions: level_1, 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/select.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {css} from 'emotion'; 3 | 4 | import {ice, white} from '../variables/colors'; 5 | import { 6 | spacingExtraSmall, spacingLarge, spacingNormal, spacingSmall, 7 | } from '../variables/spacing'; 8 | 9 | 10 | // Adapted from https://www.filamentgroup.com/lab/select-css.html 11 | // (via https://css-tricks.com/styling-a-select-like-its-2019/) 12 | // Caret SVG: 13 | // 14 | 15 | const select = css` 16 | display: inline-block; 17 | font-size: inherit; 18 | font-family: inherit; 19 | font-weight: inherit; 20 | color: inherit; 21 | line-height: ${spacingNormal}; 22 | padding: ${spacingExtraSmall} ${spacingLarge} ${spacingExtraSmall} ${spacingSmall}; 23 | width: auto; 24 | max-width: 100%; 25 | box-sizing: border-box; 26 | margin: 0; 27 | border: 1px solid ${ice}; 28 | border-radius: ${spacingNormal}; 29 | -moz-appearance: none; 30 | -webkit-appearance: none; 31 | appearance: none; 32 | background-color: ${white}; 33 | background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9IiM4MTkwOWQiIGZvY3VzYWJsZT0iZmFsc2UiIHZpZXdCb3g9IjAgMCAyNCAyNCIgYXJpYS1oaWRkZW49InRydWUiIHJvbGU9InByZXNlbnRhdGlvbiI+PHBhdGggZD0iTTcgMTBsNSA1IDUtNXoiPjwvcGF0aD48L3N2Zz4='); 34 | background-repeat: no-repeat; 35 | background-position: right 3px top 50%; 36 | background-size: 24px 24px; 37 | cursor: pointer; 38 | 39 | &::-ms-expand { 40 | display: none; 41 | } 42 | 43 | &:hover { 44 | border-color: ${ice}; 45 | background-color: ${ice}; 46 | } 47 | 48 | &:focus { 49 | border-color: ${ice}; 50 | outline: none; 51 | } 52 | 53 | & option { 54 | font-weight:normal; 55 | } 56 | `; 57 | 58 | const Select = props => ( 59 | 8 | 9 | 10 | 11 | )); 12 | -------------------------------------------------------------------------------- /src/components/tabs.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {css} from 'emotion'; 3 | import { 4 | Tab as ReactTab, TabList as ReactTabList, 5 | } from 'react-tabs'; 6 | import { 7 | blueGrey, ice, tealish, white, 8 | } from '../variables/colors'; 9 | import {spacingNormal, spacingSmall} from '../variables/spacing'; 10 | 11 | export {Tabs, TabPanel} from 'react-tabs'; 12 | 13 | const cssTabList = ` 14 | border-bottom: 1px solid ${ice}; 15 | padding: 0; 16 | margin: ${spacingNormal} 0; 17 | display: flex; 18 | list-style: none; 19 | `; 20 | 21 | export const cssTabTemplate = ` 22 | padding: ${spacingSmall} ${spacingNormal}; 23 | border: 1px solid transparent; 24 | cursor: pointer; 25 | border-top-left-radius: 3px; 26 | border-top-right-radius: 3px; 27 | margin-bottom: -1px; 28 | 29 | &.selected { 30 | border-color: ${ice} ${ice} ${white} ${ice}; 31 | color: ${tealish}; 32 | } 33 | 34 | &.disabled { 35 | cursor: not-allowed; 36 | color: ${blueGrey}; 37 | } 38 | `; 39 | 40 | const tabList = css` 41 | ${cssTabList} 42 | `; 43 | 44 | const tabTemplate = css` 45 | ${cssTabTemplate} 46 | `; 47 | 48 | export const TabList = props => ( 49 | 53 | ); 54 | TabList.tabsRole = 'TabList'; 55 | 56 | export const Tab = props => ( 57 | 63 | ); 64 | Tab.tabsRole = 'Tab'; 65 | -------------------------------------------------------------------------------- /src/components/tabs.stories.js: -------------------------------------------------------------------------------- 1 | import {storiesOf} from '@storybook/react'; 2 | import React from 'react'; 3 | import {css} from 'emotion'; 4 | import {color, withKnobs} from '@storybook/addon-knobs'; 5 | import { 6 | TabList, 7 | Tab, 8 | Tabs, 9 | TabPanel, 10 | cssTabTemplate, 11 | } from './tabs'; 12 | import {gold} from '../variables/colors'; 13 | 14 | storiesOf('Tabs', module) 15 | .addDecorator(withKnobs) 16 | .add('default', () => ( 17 | 18 | 19 | Description 20 | Metrics 21 | 22 | 23 |

First tab's content

24 |
25 | 26 |

Second tab's content

27 |
28 |
29 | )) 30 | .add('color override', () => { 31 | const checkColor = color('Color: ', gold); 32 | const tabStyle = css` 33 | ${cssTabTemplate}; 34 | &.selected { 35 | color: ${checkColor}; 36 | } 37 | `; 38 | const MyTab = props => ; 39 | MyTab.tabsRole = 'Tab'; 40 | return ( 41 | 42 | 43 | Description 44 | Metrics 45 | 46 | 47 |

First tab's content

48 |
49 | 50 |

Second tab's content

51 |
52 |
53 | ); 54 | }) 55 | .add('disabled', () => ( 56 | 57 | 58 | Description 59 | Metrics 60 | 61 | 62 |

First tab's content

63 |
64 | 65 |

Second tab's content

66 |
67 |
68 | )); 69 | -------------------------------------------------------------------------------- /src/components/tabs.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, fireEvent} from '@testing-library/react'; 3 | import { 4 | TabList, 5 | Tab, 6 | Tabs, 7 | TabPanel, 8 | } from './tabs'; 9 | import {tealish} from '../variables/colors'; 10 | 11 | test('Tab\'s color changes & the description is updated', () => { 12 | const {getByTestId} = render( 13 | 14 | Description 15 | Metrics 16 | 17 | 18 |

First tab's content

19 |
20 | 21 |

Second tab's content

22 |
23 |
); 24 | 25 | expect(getByTestId('desc_1')).toBeDefined(); 26 | expect(() => getByTestId('desc_2')).toThrow(); 27 | 28 | expect(getByTestId('title_1')).toHaveStyle(`color: ${tealish}`); 29 | expect(getByTestId('title_2')).not.toHaveStyle(`color: ${tealish}`); 30 | 31 | fireEvent.click(getByTestId('title_2')); 32 | 33 | expect(() => getByTestId('desc_1')).toThrow(); 34 | expect(getByTestId('desc_2')).toBeDefined(); 35 | 36 | expect(getByTestId('title_1')).not.toHaveStyle(`color: ${tealish}`); 37 | expect(getByTestId('title_2')).toHaveStyle(`color: ${tealish}`); 38 | }); 39 | test('It should have a disabled state', () => { 40 | const {getByTestId} = render( 41 | 42 | Description 43 | Metrics 44 | 45 | 46 |

First tab's content

47 |
48 | 49 |

Second tab's content

50 |
51 |
); 52 | 53 | expect(getByTestId('desc_1')).toBeDefined(); 54 | expect(() => getByTestId('desc_2')).toThrow(); 55 | 56 | expect(getByTestId('title_1')).toHaveStyle(`color: ${tealish}`); 57 | expect(getByTestId('title_2')).not.toHaveStyle(`color: ${tealish}`); 58 | 59 | fireEvent.click(getByTestId('title_2')); 60 | 61 | expect(getByTestId('desc_1')).toBeDefined(); 62 | expect(() => getByTestId('desc_2')).toThrow(); 63 | 64 | expect(getByTestId('title_1')).toHaveStyle(`color: ${tealish}`); 65 | expect(getByTestId('title_2')).not.toHaveStyle(`color: ${tealish}`); 66 | }); 67 | -------------------------------------------------------------------------------- /src/components/twoPanelLayout.js: -------------------------------------------------------------------------------- 1 | /* global window */ 2 | import React, {Component, Fragment} from 'react'; 3 | import {css} from 'emotion'; 4 | import PropTypes from '../utils/propTypes'; 5 | import {spacingLarge, spacingNormal} from '../variables/spacing'; 6 | import {white, ice} from '../variables/colors'; 7 | 8 | const MIN_COL_WIDTH = 250; 9 | 10 | export const middle = css` 11 | display: inline-block; 12 | vertical-align: top; 13 | `; 14 | 15 | export const margin = 40; 16 | const barSize = 15; 17 | const halfBarSize = (barSize - 1) / 2; 18 | 19 | export const verticalBar = css` 20 | ${middle}; 21 | width: ${barSize}px; 22 | margin-right: -${halfBarSize}px; 23 | margin-left: -${halfBarSize}px; 24 | z-index: 1; 25 | cursor: col-resize; 26 | background-color: transparent; 27 | flex-grow: 0; 28 | flex-shrink: 0; 29 | 30 | position: relative; 31 | :before { 32 | content: ""; 33 | position: absolute; 34 | top: 0; 35 | bottom: 0; 36 | left: ${halfBarSize}px; 37 | border-left: 1px solid ${ice}; 38 | } 39 | `; 40 | 41 | 42 | class TwoPanelLayout extends Component { 43 | state = { 44 | hold: false, 45 | }; 46 | 47 | constructor(props) { 48 | super(props); 49 | const {defaultLeftPanelWidth} = this.props; 50 | this.state.leftPanelWidth = defaultLeftPanelWidth; 51 | this.contentRef = React.createRef(); 52 | } 53 | 54 | componentDidMount() { 55 | this.updateDimensions(); 56 | window.addEventListener('resize', this.updateDimensions); 57 | } 58 | 59 | componentWillUnmount() { 60 | window.removeEventListener('resize', this.updateDimensions); 61 | } 62 | 63 | updateDimensions = () => { 64 | if (this.contentRef.current) { 65 | const containerWidth = this.contentRef.current.offsetWidth; 66 | const leftPanelWidth = this.state.leftPanelWidth.unit === '%' ? this.state.leftPanelWidth.value * containerWidth / 100 : this.state.leftPanelWidth.value; 67 | 68 | this.updateLeftPanelWidth(containerWidth, leftPanelWidth); 69 | } 70 | }; 71 | 72 | move = e => { 73 | if (this.state.hold) { 74 | e.persist(); 75 | 76 | const containerWidth = e.currentTarget.offsetWidth; 77 | const leftPanelWidth = e.clientX - margin - 1; 78 | this.updateLeftPanelWidth(containerWidth, leftPanelWidth); 79 | } 80 | }; 81 | 82 | updateLeftPanelWidth(containerWidth, leftPanelWidth) { 83 | const MAX_COL_WIDTH = Math.max(0, containerWidth - MIN_COL_WIDTH); 84 | const actualLeftPanelWidth = Math.min(Math.max(MIN_COL_WIDTH, leftPanelWidth), MAX_COL_WIDTH); 85 | 86 | this.setState(state => ({ 87 | ...state, 88 | leftPanelWidth: {value: actualLeftPanelWidth, unit: 'px'}, 89 | } 90 | )); 91 | } 92 | 93 | mouseDown = () => this.setState(state => ({ 94 | ...state, 95 | hold: true, 96 | })); 97 | 98 | mouseUp = () => { 99 | if (this.state.hold) { 100 | this.setState(state => ({ 101 | ...state, 102 | hold: false, 103 | })); 104 | } 105 | }; 106 | 107 | getLayout = () => css` 108 | margin: 0 ${spacingLarge} ${spacingNormal} ${spacingLarge}; 109 | background-color: ${white}; 110 | border: 1px solid ${ice}; 111 | display: flex; 112 | flex: 1; 113 | align-items: stretch; 114 | overflow: hidden; 115 | ${this.state.hold ? ` 116 | cursor: col-resize; 117 | user-select: none; 118 | ` : ''} 119 | `; 120 | 121 | getLeftPanel = rightPanelContent => css` 122 | width: ${rightPanelContent ? `${this.state.leftPanelWidth.value}${this.state.leftPanelWidth.unit}` : '100%'}; 123 | flex-grow: 0; 124 | flex-shrink: 0; 125 | display: flex; 126 | overflow: hidden; 127 | `; 128 | 129 | getRightPanel = css` 130 | flex-grow: 1; 131 | display: flex; 132 | overflow: hidden; 133 | `; 134 | 135 | render() { 136 | const {leftPanelContent, rightPanelContent} = this.props; 137 | 138 | return ( 139 |
145 |
146 | {leftPanelContent} 147 |
148 | 149 | {rightPanelContent && ( 150 | 151 |
155 |
156 | {rightPanelContent} 157 |
158 | 159 | )} 160 |
161 | ); 162 | } 163 | } 164 | 165 | TwoPanelLayout.propTypes = { 166 | defaultLeftPanelWidth: PropTypes.shape({ 167 | value: PropTypes.number, 168 | unit: PropTypes.string, 169 | }), 170 | leftPanelContent: PropTypes.node.isRequired, 171 | rightPanelContent: PropTypes.node, 172 | }; 173 | 174 | TwoPanelLayout.defaultProps = { 175 | defaultLeftPanelWidth: {value: 40, unit: '%'}, 176 | rightPanelContent: null, 177 | }; 178 | 179 | export default TwoPanelLayout; 180 | -------------------------------------------------------------------------------- /src/components/twoPanelLayout.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {storiesOf} from '@storybook/react'; 3 | 4 | import TwoPanelLayout from './twoPanelLayout'; 5 | import {PanelContent, PanelTop, PanelWrapper} from './panel'; 6 | 7 | storiesOf('TwoPanelsLayout', module) 8 | .add('single panel', () => ( 9 | 12 | )) 13 | .add('two panels', () => ( 14 | 18 | )) 19 | .add('two panels with proper style', () => { 20 | const panelWithStyle = ( 21 | 22 | Panel top 23 | Panel content 24 | 25 | ); 26 | return ( 27 | 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /src/globalStyles/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Global, css} from '@emotion/core'; 4 | import emotionNormalize from 'emotion-normalize'; 5 | import {iceBlue, slate} from '../variables/colors'; 6 | import {fontNormal} from '../variables/font'; 7 | import {spacingSmall} from '../variables/spacing'; 8 | 9 | // unused for now 10 | // const fontFaceItalic = (variant, weight) => css` 11 | // ${fontFace(variant, 'normal', weight)} 12 | // ${fontFace(`${variant}Italic`, 'italic', weight)} 13 | // `; 14 | 15 | 16 | // hardcode which font we want for avoiding making webpack load all available fonts 17 | const fontFaceNormal = ` 18 | @font-face { 19 | font-family: 'Lato'; 20 | font-style: normal; 21 | font-weight: 700; 22 | src: url(${require('./lato/LatoLatin-Bold.eot')}); /* IE9 Compat Modes */ 23 | src: url('${require('./lato/LatoLatin-Bold.eot')}?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 24 | url(${require('./lato/LatoLatin-Bold.woff2')}) format('woff2'), /* Super Modern Browsers */ 25 | url(${require('./lato/LatoLatin-Bold.woff')}) format('woff'), /* Pretty Modern Browsers */ 26 | url(${require('./lato/LatoLatin-Bold.ttf')}) format('truetype'); /* Safari, Android, iOS */ 27 | }; 28 | @font-face { 29 | font-family: 'Lato'; 30 | font-style: normal; 31 | font-weight: 400; 32 | src: url(${require('./lato/LatoLatin-Regular.eot')}); /* IE9 Compat Modes */ 33 | src: url('${require('./lato/LatoLatin-Regular.eot')}?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 34 | url(${require('./lato/LatoLatin-Regular.woff2')}) format('woff2'), /* Super Modern Browsers */ 35 | url(${require('./lato/LatoLatin-Regular.woff')}) format('woff'), /* Pretty Modern Browsers */ 36 | url(${require('./lato/LatoLatin-Regular.ttf')}) format('truetype'); /* Safari, Android, iOS */ 37 | }; 38 | `; 39 | 40 | 41 | const globalStyles = css` 42 | ${emotionNormalize} 43 | 44 | ${fontFaceNormal}; 45 | 46 | html { 47 | box-sizing: border-box; 48 | } 49 | 50 | *, *:before, *:after { 51 | box-sizing: inherit; 52 | } 53 | 54 | html, body, #root, #root > div { 55 | height: 100%; 56 | } 57 | 58 | body { 59 | margin: 0; 60 | padding: 0; 61 | background: ${iceBlue}; 62 | color: ${slate}; 63 | font-family: 'Lato', sans-serif; 64 | font-size: ${fontNormal}; 65 | letter-spacing: 0.5px; 66 | 67 | -webkit-font-smoothing: antialiased; /* This needs to be set or some font faced fonts look bold on Mac in Chrome/Webkit based browsers. */ 68 | -moz-osx-font-smoothing: grayscale; /* Fixes font bold issue in Firefox version 25+ on Mac */ 69 | } 70 | 71 | h1, h2, h3, h4 { 72 | margin: 0; 73 | } 74 | 75 | button, 76 | html [type="button"], /* 1 */ 77 | [type="reset"], 78 | [type="submit"] { 79 | -webkit-appearance: none; /* 2 */ 80 | } 81 | 82 | .error { color: #ba0000; } 83 | 84 | span.error { 85 | display: block; 86 | margin-top: ${spacingSmall}; 87 | font-size: ${fontNormal}; 88 | } 89 | 90 | dl { display: inline-block;vertical-align: top; } 91 | dd, dt { display: inline-block; } 92 | dt { width: 45%;padding-right: 5%; } 93 | dd { width: 50%; } 94 | `; 95 | 96 | const GlobalStyles = () => ; 97 | 98 | export default GlobalStyles; 99 | -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Black.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Black.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Black.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Black.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Black.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-BlackItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-BlackItalic.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-BlackItalic.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-BlackItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-BlackItalic.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-BlackItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-BlackItalic.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Bold.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Bold.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Bold.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Bold.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-BoldItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-BoldItalic.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-BoldItalic.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-BoldItalic.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-BoldItalic.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Hairline.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Hairline.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Hairline.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Hairline.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Hairline.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Hairline.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Hairline.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Hairline.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-HairlineItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-HairlineItalic.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-HairlineItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-HairlineItalic.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-HairlineItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-HairlineItalic.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-HairlineItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-HairlineItalic.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Heavy.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Heavy.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Heavy.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Heavy.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Heavy.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Heavy.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Heavy.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Heavy.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-HeavyItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-HeavyItalic.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-HeavyItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-HeavyItalic.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-HeavyItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-HeavyItalic.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-HeavyItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-HeavyItalic.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Light.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Light.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Light.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Light.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-LightItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-LightItalic.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-LightItalic.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-LightItalic.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-LightItalic.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Medium.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Medium.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Medium.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Medium.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Medium.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-MediumItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-MediumItalic.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-MediumItalic.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-MediumItalic.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-MediumItalic.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Regular.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Regular.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Regular.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Regular.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-RegularItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-RegularItalic.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-RegularItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-RegularItalic.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-RegularItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-RegularItalic.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-RegularItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-RegularItalic.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Semibold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Semibold.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Semibold.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Semibold.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Semibold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Semibold.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-SemiboldItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-SemiboldItalic.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-SemiboldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-SemiboldItalic.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-SemiboldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-SemiboldItalic.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-SemiboldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-SemiboldItalic.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Thin.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Thin.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Thin.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Thin.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-Thin.woff2 -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-ThinItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-ThinItalic.eot -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-ThinItalic.ttf -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-ThinItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-ThinItalic.woff -------------------------------------------------------------------------------- /src/globalStyles/lato/LatoLatin-ThinItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabeliaLabs/substra-ui/c5e9c7bacfb308e55fe29116bfa8183363daffcb/src/globalStyles/lato/LatoLatin-ThinItalic.woff2 -------------------------------------------------------------------------------- /src/icons/alert.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {gold} from '../variables/colors'; 4 | 5 | const Alert = ({ 6 | className, width, height, color, ...props 7 | }) => ( 8 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | 29 | Alert.defaultProps = { 30 | className: '', 31 | width: 24, 32 | height: 24, 33 | color: gold, 34 | }; 35 | 36 | Alert.propTypes = { 37 | className: PropTypes.string, 38 | width: PropTypes.number, 39 | height: PropTypes.number, 40 | color: PropTypes.string, 41 | }; 42 | 43 | export default Alert; 44 | -------------------------------------------------------------------------------- /src/icons/algo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate} from '../variables/colors'; 4 | 5 | const Algo = ({ 6 | className, width, height, color, ...props 7 | }) => ( 8 | 16 | 17 | 21 | 22 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | ); 36 | 37 | Algo.defaultProps = { 38 | className: '', 39 | width: 45, 40 | height: 25, 41 | color: slate, 42 | }; 43 | 44 | Algo.propTypes = { 45 | className: PropTypes.string, 46 | width: PropTypes.number, 47 | height: PropTypes.number, 48 | color: PropTypes.string, 49 | }; 50 | 51 | export default Algo; 52 | -------------------------------------------------------------------------------- /src/icons/book.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate} from '../variables/colors'; 4 | 5 | const Book = ({ 6 | className, width, height, color, ...props 7 | }) => ( 8 | 16 | 21 | 22 | ); 23 | 24 | Book.defaultProps = { 25 | className: '', 26 | width: 24, 27 | height: 24, 28 | color: slate, 29 | }; 30 | 31 | Book.propTypes = { 32 | className: PropTypes.string, 33 | width: PropTypes.number, 34 | height: PropTypes.number, 35 | color: PropTypes.string, 36 | }; 37 | 38 | export default Book; 39 | -------------------------------------------------------------------------------- /src/icons/check.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Check = ({ 5 | className, width, height, color, ...props 6 | }) => ( 7 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | Check.defaultProps = { 22 | className: '', 23 | width: 24, 24 | height: 24, 25 | color: '#28a745', 26 | }; 27 | 28 | Check.propTypes = { 29 | className: PropTypes.string, 30 | width: PropTypes.number, 31 | height: PropTypes.number, 32 | color: PropTypes.string, 33 | }; 34 | 35 | export default Check; 36 | -------------------------------------------------------------------------------- /src/icons/clear.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate} from '../variables/colors'; 4 | 5 | const ClearIcon = ({ 6 | className, width, height, color, ...props 7 | }) => ( 8 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | 29 | ClearIcon.defaultProps = { 30 | className: '', 31 | width: 24, 32 | height: 24, 33 | color: slate, 34 | }; 35 | 36 | ClearIcon.propTypes = { 37 | className: PropTypes.string, 38 | width: PropTypes.number, 39 | height: PropTypes.number, 40 | color: PropTypes.string, 41 | }; 42 | 43 | export default ClearIcon; 44 | -------------------------------------------------------------------------------- /src/icons/clipboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate} from '../variables/colors'; 4 | 5 | const Clipboard = ({ 6 | className, width, height, color, ...props 7 | }) => ( 8 | 16 | 17 | 21 | 22 | 23 | ); 24 | 25 | Clipboard.defaultProps = { 26 | className: '', 27 | width: 24, 28 | height: 24, 29 | color: slate, 30 | }; 31 | 32 | Clipboard.propTypes = { 33 | className: PropTypes.string, 34 | width: PropTypes.number, 35 | height: PropTypes.number, 36 | color: PropTypes.string, 37 | }; 38 | 39 | export default Clipboard; 40 | -------------------------------------------------------------------------------- /src/icons/collapse.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate} from '../variables/colors'; 4 | 5 | const Collapse = ({ 6 | className, width, height, color, ...props 7 | }) => ( 8 | 16 | 22 | 25 | 26 | 27 | ); 28 | 29 | Collapse.defaultProps = { 30 | className: '', 31 | width: 22, 32 | height: 22, 33 | color: slate, 34 | }; 35 | 36 | Collapse.propTypes = { 37 | className: PropTypes.string, 38 | width: PropTypes.number, 39 | height: PropTypes.number, 40 | color: PropTypes.string, 41 | }; 42 | 43 | export default Collapse; 44 | -------------------------------------------------------------------------------- /src/icons/copyDrop.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate, tealish} from '../variables/colors'; 4 | 5 | const CopyDrop = ({ 6 | className, width, height, color, secondaryColor, ...props 7 | }) => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | 42 | CopyDrop.defaultProps = { 43 | className: '', 44 | width: 24, 45 | height: 24, 46 | color: slate, 47 | secondaryColor: tealish, 48 | }; 49 | 50 | CopyDrop.propTypes = { 51 | className: PropTypes.string, 52 | width: PropTypes.number, 53 | height: PropTypes.number, 54 | color: PropTypes.string, 55 | secondaryColor: PropTypes.string, 56 | }; 57 | 58 | export default CopyDrop; 59 | -------------------------------------------------------------------------------- /src/icons/copySimple.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate} from '../variables/colors'; 4 | 5 | const CopySimple = ({ 6 | className, width, height, color, ...props 7 | }) => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | 33 | CopySimple.defaultProps = { 34 | className: '', 35 | width: 28, 36 | height: 29, 37 | color: slate, 38 | }; 39 | 40 | CopySimple.propTypes = { 41 | className: PropTypes.string, 42 | width: PropTypes.number, 43 | height: PropTypes.number, 44 | color: PropTypes.string, 45 | }; 46 | 47 | export default CopySimple; 48 | -------------------------------------------------------------------------------- /src/icons/dataset.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate} from '../variables/colors'; 4 | 5 | const Dataset = ({ 6 | className, width, height, color, ...props 7 | }) => ( 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | 25 | Dataset.defaultProps = { 26 | className: '', 27 | width: 23, 28 | height: 25, 29 | color: slate, 30 | }; 31 | 32 | Dataset.propTypes = { 33 | className: PropTypes.string, 34 | width: PropTypes.number, 35 | height: PropTypes.number, 36 | color: PropTypes.string, 37 | }; 38 | 39 | export default Dataset; 40 | -------------------------------------------------------------------------------- /src/icons/downloadDrop.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate, tealish} from '../variables/colors'; 4 | 5 | const DownloadDrop = ({ 6 | className, width, height, color, secondaryColor, ...props 7 | }) => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | 25 | DownloadDrop.defaultProps = { 26 | className: '', 27 | width: 24, 28 | height: 24, 29 | color: slate, 30 | secondaryColor: tealish, 31 | }; 32 | 33 | DownloadDrop.propTypes = { 34 | className: PropTypes.string, 35 | width: PropTypes.number, 36 | height: PropTypes.number, 37 | color: PropTypes.string, 38 | secondaryColor: PropTypes.string, 39 | }; 40 | 41 | export default DownloadDrop; 42 | -------------------------------------------------------------------------------- /src/icons/downloadSimple.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate} from '../variables/colors'; 4 | 5 | const DownloadSimple = ({ 6 | className, width, height, color, ...props 7 | }) => ( 8 | 16 | 17 | 18 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | ); 34 | 35 | DownloadSimple.defaultProps = { 36 | className: '', 37 | width: 24, 38 | height: 24, 39 | color: slate, 40 | }; 41 | 42 | DownloadSimple.propTypes = { 43 | className: PropTypes.string, 44 | width: PropTypes.number, 45 | height: PropTypes.number, 46 | color: PropTypes.string, 47 | }; 48 | 49 | export default DownloadSimple; 50 | -------------------------------------------------------------------------------- /src/icons/expand.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate} from '../variables/colors'; 4 | 5 | const Expand = ({ 6 | className, width, height, color, ...props 7 | }) => ( 8 | 16 | 22 | 25 | 26 | 27 | ); 28 | 29 | Expand.defaultProps = { 30 | className: '', 31 | width: 22, 32 | height: 22, 33 | color: slate, 34 | }; 35 | 36 | Expand.propTypes = { 37 | className: PropTypes.string, 38 | width: PropTypes.number, 39 | height: PropTypes.number, 40 | color: PropTypes.string, 41 | }; 42 | 43 | export default Expand; 44 | -------------------------------------------------------------------------------- /src/icons/filterUp.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate, tealish} from '../variables/colors'; 4 | 5 | const FilterUp = ({ 6 | className, width, height, color, secondaryColor, ...props 7 | }) => ( 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | 27 | FilterUp.defaultProps = { 28 | color: slate, 29 | secondaryColor: tealish, 30 | className: '', 31 | width: 24, 32 | height: 24, 33 | }; 34 | 35 | FilterUp.propTypes = { 36 | color: PropTypes.string, 37 | secondaryColor: PropTypes.string, 38 | className: PropTypes.string, 39 | width: PropTypes.number, 40 | height: PropTypes.number, 41 | }; 42 | 43 | export default FilterUp; 44 | -------------------------------------------------------------------------------- /src/icons/folder.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate} from '../variables/colors'; 4 | 5 | const Folder = ({ 6 | className, width, height, color, ...props 7 | }) => ( 8 | 16 | 20 | 21 | 22 | 23 | 24 | ); 25 | 26 | Folder.defaultProps = { 27 | className: '', 28 | width: 24, 29 | height: 24, 30 | color: slate, 31 | }; 32 | 33 | Folder.propTypes = { 34 | className: PropTypes.string, 35 | width: PropTypes.number, 36 | height: PropTypes.number, 37 | color: PropTypes.string, 38 | }; 39 | 40 | export default Folder; 41 | -------------------------------------------------------------------------------- /src/icons/icons.stories.js: -------------------------------------------------------------------------------- 1 | import React, {Fragment} from 'react'; 2 | import {storiesOf} from '@storybook/react'; 3 | import {withKnobs, color, number} from '@storybook/addon-knobs/react'; 4 | 5 | import styled from '@emotion/styled'; 6 | import { 7 | Alert, 8 | Algo, 9 | Book, 10 | Check, 11 | ClearIcon, 12 | Clipboard, 13 | Collapse, 14 | CopyDrop, 15 | CopySimple, 16 | Dataset, 17 | DownloadDrop, 18 | DownloadSimple, 19 | Expand, 20 | FilterUp, 21 | Folder, 22 | Model, 23 | MoreVertical, 24 | OwkestraLogo, 25 | Permission, 26 | Search, 27 | SubstraLogo, 28 | } from './index'; 29 | import {slate, tealish} from '../variables/colors'; 30 | import {spacingSmall} from '../variables/spacing'; 31 | 32 | const Dl = styled.dl` 33 | display: grid; 34 | grid-template-columns: 50px auto; 35 | grid-gap: ${spacingSmall}; 36 | `; 37 | 38 | const Dt = styled.dt` 39 | grid-column: 1; 40 | width: 50px; 41 | `; 42 | 43 | const Dd = styled.dd` 44 | grid-column: 2; 45 | margin: 0; 46 | `; 47 | 48 | storiesOf('Icons', module) 49 | .addDecorator(withKnobs) 50 | .add('default', () => { 51 | const colorKnob = color('color', slate); 52 | const secondaryColorKnob = color('secondaryColor', tealish); 53 | const heightKnob = number('height', 24); 54 | const widthKnob = number('width', 24); 55 | return ( 56 | 57 |
58 |
59 | 60 |
61 |
62 | Alert 63 |
64 |
65 | 66 |
67 |
68 | Algo 69 |
70 |
71 | 72 |
73 |
74 | Book 75 |
76 |
77 | 78 |
79 |
80 | Check 81 |
82 |
83 | 84 |
85 |
86 | ClearIcon 87 |
88 |
89 | 90 |
91 |
92 | Clipboard 93 |
94 |
95 | 96 |
97 |
98 | Collapse 99 |
100 |
101 | 102 |
103 |
104 | CopyDrop 105 |
106 |
107 | 108 |
109 |
110 | CopySimple 111 |
112 |
113 | 114 |
115 |
116 | Dataset 117 |
118 |
119 | 120 |
121 |
122 | DownloadDrop 123 |
124 |
125 | 126 |
127 |
128 | DownloadSimple 129 |
130 |
131 | 132 |
133 |
134 | Expand 135 |
136 |
137 | 138 |
139 |
140 | FilterUp 141 |
142 |
143 | 144 |
145 |
146 | Folder 147 |
148 |
149 | 150 |
151 |
152 | Model 153 |
154 |
155 | 156 |
157 |
158 | MoreVertical 159 |
160 |
161 | 162 |
163 |
164 | Permission 165 |
166 |
167 | 168 |
169 |
170 | Search 171 |
172 |
173 |
174 | ); 175 | }) 176 | .add('logos', () => ( 177 | 178 |
179 | 180 |
181 |
182 | 183 |
184 |
185 | )); 186 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | export {default as Alert} from './alert'; 2 | export {default as Algo} from './algo'; 3 | export {default as Book} from './book'; 4 | export {default as Check} from './check'; 5 | export {default as ClearIcon} from './clear'; 6 | export {default as Clipboard} from './clipboard'; 7 | export {default as Collapse} from './collapse'; 8 | export {default as CopyDrop} from './copyDrop'; 9 | export {default as CopySimple} from './copySimple'; 10 | export {default as Dataset} from './dataset'; 11 | export {default as DownloadDrop} from './downloadDrop'; 12 | export {default as DownloadSimple} from './downloadSimple'; 13 | export {default as Expand} from './expand'; 14 | export {default as FilterUp} from './filterUp'; 15 | export {default as Folder} from './folder'; 16 | export {default as Model} from './model'; 17 | export {default as MoreVertical} from './moreVertical'; 18 | export {default as OwkestraLogo} from './owkestraLogo'; 19 | export {default as Permission} from './permission'; 20 | export {default as Search} from './search'; 21 | export {default as SubstraLogo} from './substraLogo'; 22 | -------------------------------------------------------------------------------- /src/icons/model.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate} from '../variables/colors'; 4 | 5 | const Model = ({ 6 | className, width, height, color, ...props 7 | }) => ( 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | 30 | Model.defaultProps = { 31 | className: '', 32 | width: 24, 33 | height: 22, 34 | color: slate, 35 | }; 36 | 37 | Model.propTypes = { 38 | className: PropTypes.string, 39 | width: PropTypes.number, 40 | height: PropTypes.number, 41 | color: PropTypes.string, 42 | }; 43 | 44 | export default Model; 45 | -------------------------------------------------------------------------------- /src/icons/moreVertical.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate} from '../variables/colors'; 4 | 5 | const MoreVertical = ({ 6 | className, width, height, color, ...props 7 | }) => ( 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | 24 | MoreVertical.defaultProps = { 25 | className: '', 26 | width: 24, 27 | height: 24, 28 | color: slate, 29 | }; 30 | 31 | MoreVertical.propTypes = { 32 | className: PropTypes.string, 33 | width: PropTypes.number, 34 | height: PropTypes.number, 35 | color: PropTypes.string, 36 | }; 37 | 38 | export default MoreVertical; 39 | -------------------------------------------------------------------------------- /src/icons/owkestraLogo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default props => ( 4 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 35 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 76 | 77 | Owkestra Logo 78 | 82 | 86 | 90 | 94 | 98 | 102 | 106 | 110 | 114 | 118 | 122 | 126 | 130 | 134 | 138 | 142 | 146 | 150 | 151 | ); 152 | -------------------------------------------------------------------------------- /src/icons/permission.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Permission = ({ 5 | className, width, height, color, ...props 6 | }) => ( 7 | 15 | 16 | 20 | 21 | 22 | 23 | ); 24 | 25 | Permission.defaultProps = { 26 | className: '', 27 | width: 8, 28 | height: 9, 29 | color: '#4C9BBA', 30 | }; 31 | 32 | Permission.propTypes = { 33 | className: PropTypes.string, 34 | width: PropTypes.number, 35 | height: PropTypes.number, 36 | color: PropTypes.string, 37 | }; 38 | 39 | export default Permission; 40 | -------------------------------------------------------------------------------- /src/icons/search.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate} from '../variables/colors'; 4 | 5 | const Search = ({ 6 | className, width, height, color, ...props 7 | }) => ( 8 | 16 | 17 | 22 | 23 | 24 | ); 25 | 26 | Search.defaultProps = { 27 | className: '', 28 | width: 24, 29 | height: 24, 30 | color: slate, 31 | }; 32 | 33 | Search.propTypes = { 34 | className: PropTypes.string, 35 | width: PropTypes.number, 36 | height: PropTypes.number, 37 | color: PropTypes.string, 38 | }; 39 | 40 | export default Search; 41 | -------------------------------------------------------------------------------- /src/icons/substraLogo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {slate, tealish} from '../variables/colors'; 4 | 5 | const SubstraLogo = ({ 6 | className, width, height, ...props 7 | }) => ( 8 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 35 | 36 | 37 | ); 38 | 39 | SubstraLogo.defaultProps = { 40 | className: '', 41 | width: 340, 42 | height: 64, 43 | }; 44 | 45 | SubstraLogo.propTypes = { 46 | className: PropTypes.string, 47 | width: PropTypes.number, 48 | height: PropTypes.number, 49 | }; 50 | 51 | export default SubstraLogo; 52 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Components 2 | export {Button, RoundedButton} from './components/roundedButton'; 3 | export {IconButton, RoundButton} from './components/iconButton'; 4 | export CodeSample from './components/codeSample'; 5 | export CopyInput from './components/copyInput'; 6 | export SearchBar from './components/searchBar/searchBar'; 7 | export withAddNotification from './components/copyNotification/copyNotification'; 8 | export { 9 | TabList, 10 | Tab, 11 | Tabs, 12 | TabPanel, 13 | cssTabTemplate, 14 | } from './components/tabs'; 15 | export {PanelWrapper, PanelTop, PanelContent} from './components/panel'; 16 | export TwoPanelLayout from './components/twoPanelLayout'; 17 | export Select from './components/select'; 18 | 19 | export { 20 | alertWrapper, 21 | alertTitle, 22 | AlertActions, 23 | alertInlineButton, 24 | } from './components/alert'; 25 | 26 | // Global styles 27 | export GlobalStyles from './globalStyles'; 28 | 29 | // Icons 30 | export { 31 | Alert, 32 | Algo, 33 | Book, 34 | Check, 35 | Clipboard, 36 | Collapse, 37 | CopyDrop, 38 | CopySimple, 39 | Dataset, 40 | DownloadDrop, 41 | DownloadSimple, 42 | Expand, 43 | FilterUp, 44 | Folder, 45 | Model, 46 | MoreVertical, 47 | OwkestraLogo, 48 | Permission, 49 | Search, 50 | SubstraLogo, 51 | } from './icons'; 52 | 53 | // Variables 54 | export colors from './variables/colors'; 55 | export font from './variables/font'; 56 | export spacing from './variables/spacing'; 57 | -------------------------------------------------------------------------------- /src/storybook.test.js: -------------------------------------------------------------------------------- 1 | import initStoryshots from '@storybook/addon-storyshots'; 2 | 3 | initStoryshots(); 4 | -------------------------------------------------------------------------------- /src/utils/propTypes.js: -------------------------------------------------------------------------------- 1 | import RootPropTypes from 'prop-types'; 2 | import {isValidElementType} from 'react-is'; 3 | 4 | 5 | /* 6 | * Custom component propTypes 7 | * Adapted from https://github.com/facebook/react/issues/5143 and https://github.com/facebook/react/issues/9125 8 | */ 9 | const createComponentPropType = isRequired => (props, propName, componentName) => { 10 | const prop = props[propName]; 11 | if (!prop && isRequired) { 12 | throw new Error(`Missing required prop ${propName}`); 13 | } 14 | else if (prop && !isValidElementType(prop)) { 15 | return new Error(`Invalid prop '${propName}' supplied to '${componentName}': the prop is not a valid React component`); 16 | } 17 | }; 18 | 19 | const componentPropType = createComponentPropType(false); 20 | componentPropType.isRequired = createComponentPropType(true); 21 | 22 | 23 | const PropTypes = { 24 | ...RootPropTypes, 25 | component: componentPropType, 26 | }; 27 | 28 | export default PropTypes; 29 | -------------------------------------------------------------------------------- /src/variables/colors.js: -------------------------------------------------------------------------------- 1 | export const white = '#ffffff', 2 | iceBlue = '#f7f8f8', 3 | ice = '#e7e8e8', 4 | blueGrey = '#81909d', 5 | gold = '#edc20f', 6 | iceGold = '#fdf8e7', 7 | slate = '#4b6073', 8 | tealish = '#1dbcc0', 9 | darkSkyBlue = '#4ba5d2', 10 | iceBlueTwo = '#edf6fa'; 11 | 12 | export default { 13 | white, 14 | iceBlue, 15 | ice, 16 | blueGrey, 17 | gold, 18 | iceGold, 19 | slate, 20 | tealish, 21 | darkSkyBlue, 22 | iceBlueTwo, 23 | }; 24 | -------------------------------------------------------------------------------- /src/variables/font.js: -------------------------------------------------------------------------------- 1 | export const 2 | monospaceFamily = 'SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace', 3 | fontNormalMonospace = '11px', 4 | fontNormal = '12px', 5 | fontLarge = '14px'; 6 | 7 | 8 | export default { 9 | monospaceFamily, 10 | fontNormalMonospace, 11 | fontNormal, 12 | fontLarge, 13 | }; 14 | -------------------------------------------------------------------------------- /src/variables/spacing.js: -------------------------------------------------------------------------------- 1 | export const 2 | spacingExtraSmall = '5px', 3 | spacingSmall = '10px', 4 | spacingNormal = '20px', 5 | spacingLarge = '40px'; 6 | 7 | export default { 8 | spacingExtraSmall, 9 | spacingSmall, 10 | spacingNormal, 11 | spacingLarge, 12 | }; 13 | -------------------------------------------------------------------------------- /test/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /test/mocks/prismMock.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ghcolors: { 3 | 'pre[class*="language-"': {}, 4 | 'code[class*="language-"]': {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /test/mocks/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import {toHaveStyle} from '@testing-library/jest-dom'; 2 | import '@testing-library/jest-dom/extend-expect'; 3 | 4 | expect.extend({toHaveStyle}); 5 | 6 | global.Blob = (content, options) => ({content, options}); 7 | --------------------------------------------------------------------------------