├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── ---bug-report.md │ └── ---feature-request.md └── workflows │ └── build.yml ├── .gitignore ├── .storybook ├── main.js └── preview.js ├── @types └── index.d.ts ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── config.js ├── package-lock.json ├── package.json ├── setupTests.ts ├── src ├── Alert.tsx ├── Avatar.tsx ├── Backdrop.tsx ├── Badge.tsx ├── Button.tsx ├── Card.tsx ├── CardBody.tsx ├── Dropdown.tsx ├── DropdownItem.tsx ├── HelperText.tsx ├── Input.tsx ├── Label.tsx ├── Modal.tsx ├── ModalBody.tsx ├── ModalFooter.tsx ├── ModalHeader.tsx ├── Pagination.tsx ├── Select.tsx ├── Table.tsx ├── TableBody.tsx ├── TableCell.tsx ├── TableContainer.tsx ├── TableFooter.tsx ├── TableHeader.tsx ├── TableRow.tsx ├── Textarea.tsx ├── Transition.tsx ├── Windmill.tsx ├── __tests__ │ ├── Alert.test.tsx │ ├── Avatar.test.tsx │ ├── Backdrop.test.tsx │ ├── Badge.test.tsx │ ├── Button.test.tsx │ ├── Card.test.tsx │ ├── CardBody.test.tsx │ ├── Dropdown.test.tsx │ ├── DropdownItem.test.tsx │ ├── HelperText.test.tsx │ ├── Input.test.tsx │ ├── Label.test.tsx │ ├── Modal.test.tsx │ ├── ModalBody.test.tsx │ ├── ModalFooter.test.tsx │ ├── ModalHeader.test.tsx │ ├── Pagination.test.tsx │ ├── Select.test.tsx │ ├── Table.test.tsx │ ├── TableBody.test.tsx │ ├── TableCell.test.tsx │ ├── TableContainer.test.tsx │ ├── TableFooter.test.tsx │ ├── TableHeader.test.tsx │ ├── TableRow.test.tsx │ ├── Textarea.test.tsx │ ├── Transition.test.tsx │ ├── Windmill.test.tsx │ ├── context │ │ └── ThemeContext.test.tsx │ └── utils │ │ └── heart.svg ├── context │ └── ThemeContext.tsx ├── index.ts ├── stories │ ├── Alert.stories.tsx │ ├── Avatar.stories.tsx │ ├── Backdrop.stories.tsx │ ├── Badge.stories.tsx │ ├── Button.stories.tsx │ ├── Card.stories.tsx │ ├── Dropdown.stories.tsx │ ├── HelperText.stories.tsx │ ├── Input.stories.tsx │ ├── Label.stories.tsx │ ├── Modal.stories.tsx │ ├── Pagination.stories.tsx │ ├── Select.stories.tsx │ ├── Table.stories.tsx │ ├── Textarea.stories.tsx │ └── static │ │ ├── avatar-1.jpg │ │ └── heart.svg ├── themes │ └── default.ts └── utils │ ├── mergeDeep.ts │ ├── useDarkMode.ts │ └── warning.ts ├── style ├── tailwind.config.js └── tailwind.css ├── tsconfig.json └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | dev 4 | coverage 5 | lib -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "jest": true 5 | }, 6 | "settings": { 7 | "react": { 8 | "version": "detect" 9 | } 10 | }, 11 | "parserOptions": { 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "extends": ["standard", "plugin:react/recommended", "prettier"], 16 | "plugins": ["prettier"], 17 | "rules": { 18 | "no-unused-vars": [ 19 | 2, 20 | { 21 | "args": "all", 22 | "argsIgnorePattern": "^_" 23 | } 24 | ], 25 | "no-warning-comments": 0, 26 | "prettier/prettier": [ 27 | "error", 28 | { 29 | "semi": false, 30 | "singleQuote": true, 31 | "printWidth": 100, 32 | "tabWidth": 2, 33 | "useTabs": false, 34 | "trailingComma": "es5", 35 | "bracketSpacing": true, 36 | "parser": "flow", 37 | "endOfLine": "auto" 38 | } 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: 'Bugs, missing documentation, or unexpected behavior ' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 19 | 20 | - `windmill-react-ui` version: 21 | 22 | ### Relevant code or config: 23 | 24 | ```html 25 | 26 | ``` 27 | 28 | 32 | 33 | ### What you did: 34 | 35 | 36 | 37 | ### What happened: 38 | 39 | 40 | 41 | ### Reproduction: 42 | 43 | 49 | 50 | ### Problem description: 51 | 52 | 53 | 54 | ### Suggested solution: 55 | 56 | 60 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1 Feature request" 3 | about: 'I have a suggestion (and might want to implement myself)! ' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [10.x, 12.x, 14.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v1 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm ci 27 | - run: npm run lint 28 | - run: npm run test:coverage 29 | - run: npm run build 30 | - name: Upload coverage to Codecov 31 | uses: codecov/codecov-action@v1 32 | with: 33 | file: ./coverage/coverage-final.json 34 | flags: unittest 35 | name: codecov-1 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | dev 5 | style/output.css 6 | lib 7 | .env -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpackFinal: (config) => { 3 | config.resolve.extensions.push('.svg') 4 | 5 | config.module.rules = config.module.rules.map((data) => { 6 | if (/svg\|/.test(String(data.test))) 7 | data.test = /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani)(\?.*)?$/ 8 | 9 | return data 10 | }) 11 | 12 | config.module.rules.push({ 13 | test: /\.svg$/, 14 | use: [ 15 | { loader: require.resolve('babel-loader') }, 16 | { loader: require.resolve('@svgr/webpack') }, 17 | ], 18 | }) 19 | 20 | return config 21 | }, 22 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 23 | addons: ['@storybook/addon-links', '@storybook/addon-essentials'], 24 | } 25 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import '../style/output.css' 2 | 3 | export const parameters = { 4 | actions: { argTypesRegex: '^on[A-Z].*' }, 5 | } 6 | -------------------------------------------------------------------------------- /@types/index.d.ts: -------------------------------------------------------------------------------- 1 | // used in ThemeContext 2 | type Mode = string | null 3 | 4 | declare module '*.svg' { 5 | const content: any 6 | export default content 7 | } 8 | 9 | declare module '*.jpg' { 10 | const content: any 11 | export default content 12 | } 13 | 14 | declare type ListenerMap = { 15 | [k: string]: any 16 | } 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [0.6.0](https://github.com/estevanmaito/windmill-react-ui/compare/0.6.0-beta.0...0.6.0) (2021-05-17) 2 | 3 | This is the last version before 1.0. It already introduces some breaking changes from 0.5.x, but if you just want some base components on top of Tailwind 2.0, this is what you're looking for. 4 | 5 | If you need pre Tailwind v2 support, go for 0.5.1. 6 | 7 | ### BREAKING CHANGES 8 | 9 | - Upgrade Tailwind CSS to v2 10 | - Remove `tailwindcss-multi-theme` plugin 11 | - This would only be a problem for you if you're using your own solution for theme handling (not using `usePreferences` or `dark`), as we move from `theme-dark` and `theme-light` to Tailwind's own solution: `darkMode: 'class',`, which just adds `dark` to `body` as a class. 12 | - Upgrade forms plugin 13 | 14 | # [0.6.0-beta.0](https://github.com/estevanmaito/windmill-react-ui/compare/0.5.1...0.6.0-beta.0) (2021-05-15) 15 | 16 | ### Bug Fixes 17 | 18 | - **dark theme:** add dark variants to tailwind ([6e0bcd3](https://github.com/estevanmaito/windmill-react-ui/commit/6e0bcd32688924584f0a3edcab6bb7b908c3dda4)) 19 | - **dark theme:** fix focus ring for dark theme ([8095735](https://github.com/estevanmaito/windmill-react-ui/commit/8095735ab40b42b79ac0e8e5cbda695720dbdf72)) 20 | - **dark theme:** use tailwind default `dark` class to apply theme ([4c42027](https://github.com/estevanmaito/windmill-react-ui/commit/4c420279078c7a75589fed5df3a458dbffbabd03)) 21 | - **package.json:** add tailwind build before run storybook ([#37](https://github.com/estevanmaito/windmill-react-ui/issues/37)) ([6536826](https://github.com/estevanmaito/windmill-react-ui/commit/653682637554f175c7d9e0e8f7f81c3f854f2d0f)) 22 | 23 | ### Features 24 | 25 | - **dependencies:** upgrade dependencie ([0305963](https://github.com/estevanmaito/windmill-react-ui/commit/0305963609a958561223bb352ffbae856bda571a)) 26 | 27 | ### BREAKING CHANGES 28 | 29 | - **dependencies:** upgrade Tailwind to v2; remove multi-theme plugin; upgrade forms plugin; 30 | 31 | ## [0.5.1](https://github.com/estevanmaito/windmill-react-ui/compare/0.5.0...0.5.1) (2021-02-06) 32 | 33 | - **forms**: fix prop extension to allow usage of element attributes eg. name, on `input`, `select` and `textarea`. 34 | 35 | # [0.5.0](https://github.com/estevanmaito/windmill-react-ui/compare/0.4.1...0.5.0) (2020-12-02) 36 | 37 | ### Bug Fixes 38 | 39 | - **components:** add documentation for every property, for every component ([adb7bb8](https://github.com/estevanmaito/windmill-react-ui/commit/adb7bb8c33ed350c8cb6cc4bb9abfa35da631a9f)) 40 | - **events:** update event listeners to capture events ([997f8f8](https://github.com/estevanmaito/windmill-react-ui/commit/997f8f8e80d370a96e91567d2f9921767530c8a2)), closes [#20](https://github.com/estevanmaito/windmill-react-ui/issues/20) 41 | 42 | ## [0.4.1](https://github.com/estevanmaito/windmill-react-ui/compare/0.4.0...0.4.1) (2020-11-08) 43 | 44 | ### Bug Fixes 45 | 46 | - **alert:** actually export the Alert ([ccade2c](https://github.com/estevanmaito/windmill-react-ui/commit/ccade2c73fadf6ad620e249a7850b21725aa320c)) 47 | - **pagination:** fix pagination update when totalResults or resultsPerPage change ([fbedd18](https://github.com/estevanmaito/windmill-react-ui/commit/fbedd18572d64cbbc5d292d7cb8ffc0636e9ef29)) 48 | 49 | # [0.4.0](https://github.com/estevanmaito/windmill-react-ui/compare/0.3.1...0.4.0) (2020-11-08) 50 | 51 | ### Features 52 | 53 | - **alert:** add alert component ([b0855c7](https://github.com/estevanmaito/windmill-react-ui/commit/b0855c7152f71618c548876a64ccc5573e65c509)) 54 | - **TypeScript rewrite** 55 | 56 | # [0.4.0-beta.0](https://github.com/estevanmaito/windmill-react-ui/compare/0.3.1...0.4.0-beta.0) (2020-10-26) 57 | 58 | ### Bug Fixes 59 | 60 | - **button:** fix gradient style applied to anchor buttons due to type ([131bd69](https://github.com/estevanmaito/windmill-react-ui/commit/131bd69580622a98e4fddabe2330db812648c1a1)), closes [#7](https://github.com/estevanmaito/windmill-react-ui/issues/7) 61 | - **component:** add event handlers to button props ([90a3d69](https://github.com/estevanmaito/windmill-react-ui/commit/90a3d694dd82e99b8169b17c4f080c8fa994b65a)) 62 | - **packages:** update dependencies ([330a535](https://github.com/estevanmaito/windmill-react-ui/commit/330a53547028ee42da16cbb4d02157268df6e6a2)) 63 | 64 | ## [0.3.2](https://github.com/estevanmaito/windmill-react-ui/compare/0.3.1...0.3.2) (2020-08-08) 65 | 66 | ### Bug Fixes 67 | 68 | - **button:** fix gradient style applied to anchor buttons due to type being incorrectly inherited from button defaults ([131bd69](https://github.com/estevanmaito/windmill-react-ui/commit/131bd69580622a98e4fddabe2330db812648c1a1)), closes [#7](https://github.com/estevanmaito/windmill-react-ui/issues/7) 69 | 70 | ## [0.3.1](https://github.com/estevanmaito/windmill-react-ui/compare/0.3.0...0.3.1) (2020-07-21) 71 | 72 | ### Bug Fixes 73 | 74 | - **config:** fix purge content and add lib to npm published files ([abf8c24](https://github.com/estevanmaito/windmill-react-ui/commit/abf8c2425e13001700791fe0ed5cbee097dd2267)) 75 | 76 | # [0.3.0](https://github.com/estevanmaito/windmill-react-ui/compare/0.2.0...0.3.0) (2020-07-21) 77 | 78 | ### Build System 79 | 80 | - **scope:** use `@windmill` as scope ([e6862da](https://github.com/estevanmaito/windmill-react-ui/commit/e6862dab1744bc94b3a94ce8774bd93a010862af)) 81 | 82 | ### BREAKING CHANGES 83 | 84 | - **scope:** Use `@windmill/react-ui` to install the package as `windmill-react-ui` is deprecated. 85 | 86 | # [0.2.0](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.2...0.2.0) (2020-07-20) 87 | 88 | ### Bug Fixes 89 | 90 | - **form:** fix 1px off input and select ([4a3bc13](https://github.com/estevanmaito/windmill-react-ui/commit/4a3bc133f7b81744f0869b65deb136d2ed5516d1)) 91 | 92 | ### BREAKING CHANGES 93 | 94 | - **form:** Regular Inputs and Selects had a height of 39px, which looked terrible when used 95 | side-by-side with a regular button (38px height). Reducing the `line-height` to `leading-5` solved 96 | it, and now regular inputs and selects have a 38px height. 97 | 98 | ## [0.1.2](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.2-beta.0...0.1.2) (2020-07-19) 99 | 100 | ### Bug Fixes 101 | 102 | - **avatar:** add forwarRef to avatar ([3e3c8a6](https://github.com/estevanmaito/windmill-react-ui/commit/3e3c8a615d76ba52b3a2739caf2ca6cee7e2908c)) 103 | - **backdrop:** add forwardRef to backdrop ([6657d52](https://github.com/estevanmaito/windmill-react-ui/commit/6657d52a03e84b8805d8ea7ea71af719694a7d45)) 104 | - **components:** add forwardRef to all components that were missing ([fb240a2](https://github.com/estevanmaito/windmill-react-ui/commit/fb240a24da991b2a486765850e839cca5d7bbf39)) 105 | - **variants:** add default variants ([217a1d8](https://github.com/estevanmaito/windmill-react-ui/commit/217a1d8922ab18a7fe0e010b2e2cc34a4c27f460)) 106 | 107 | ### BREAKING CHANGES 108 | 109 | - **components:** All components now support ref forwarding. 110 | 111 | ## [0.1.2-beta.0](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.1...0.1.2-beta.0) (2020-07-17) 112 | 113 | ### Features 114 | 115 | - **config:** add a global config wrapper ([6a6de16](https://github.com/estevanmaito/windmill-react-ui/commit/6a6de16190479fabc2613077f1844c97716190ad)) 116 | 117 | ### BREAKING CHANGES 118 | 119 | - **config:** Instead of adding Windmill files to `purge` or to `plugins`, now you only need to 120 | encapsulate your Tailwind config using this wrapper. It will automatically add what Windmill needs 121 | to work properly and you can use your `tailwind.config.js` file as before, as it will be merged. 122 | 123 | ## [0.1.1](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0...0.1.1) (2020-07-14) 124 | 125 | ### Bug Fixes 126 | 127 | - **defaulttheme:** export an object instead of string ([9a720e5](https://github.com/estevanmaito/windmill-react-ui/commit/9a720e5db588653977107684dfa4ec4bd04fa930)) 128 | 129 | # [0.1.0](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0-alpha.14...0.1.0) (2020-07-13) 130 | 131 | ### Bug Fixes 132 | 133 | - **backdrop:** use z index 40 ([48c4348](https://github.com/estevanmaito/windmill-react-ui/commit/48c4348e6503296ddddabe5a44211f9302d88826)) 134 | - **card:** hide card overflow ([7cfa036](https://github.com/estevanmaito/windmill-react-ui/commit/7cfa036f453e33d82b1f8d554278fe7f5b754b83)) 135 | - **dropdown:** fix dropdown event handlers dependencies ([8490872](https://github.com/estevanmaito/windmill-react-ui/commit/84908725fbfe47f465b7ab2458ff6f535efb060e)) 136 | - **pagination:** add base text styles to pagination ([9258df1](https://github.com/estevanmaito/windmill-react-ui/commit/9258df1f630eecd811220cd4807172fcfd4552b1)) 137 | 138 | ### Features 139 | 140 | - **defaulttheme:** add defaultTheme export for purge ([22ac723](https://github.com/estevanmaito/windmill-react-ui/commit/22ac72365c6a565802aab592f5aac1f97c764702)) 141 | - **dropdown:** add align prop to dropdown ([7297426](https://github.com/estevanmaito/windmill-react-ui/commit/7297426205f85eaeaf1644085d5a8e0b4562a618)) 142 | - **windmill:** add usePreferences to windmill ([a619a28](https://github.com/estevanmaito/windmill-react-ui/commit/a619a283afb8829fd6659c788261fbca8197b37f)) 143 | 144 | ### BREAKING CHANGES 145 | 146 | - **pagination:** Pagination now has its own text colors, instead of depending on parent styles or 147 | own classes. It makes it useful outside elements like TableFooter, which would add text styles 148 | before. The breaking change is that it doesn't inherit TableFooter (or any parent) colors anymore. 149 | - **windmill:** Now you need to use the prop `usePreferences` in the root Windmill component if you 150 | want access to theme utilities like the current theme or theme toggler, user preferences and user 151 | theme storage. These features were enabled by default before. You get the old behavior just adding 152 | `usePreferences` to the root Windmill component. 153 | - **dropdown:** Dropdown now accepts an align prop that can be 'right' or 'left'. It now defaults 154 | to 'left' if align is not used, which is the natural align of the DOM, instead of the old right 155 | alignment. Change your dropdowns to align="right" if you were using the old default style. 156 | 157 | # [0.1.0-alpha.14](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0-alpha.13...0.1.0-alpha.14) (2020-07-10) 158 | 159 | ### Bug Fixes 160 | 161 | - **button:** fix link button active state ([cb0877e](https://github.com/estevanmaito/windmill-react-ui/commit/cb0877e19b2064a1556756da2b3ed6499ff1c8f6)) 162 | 163 | ### Features 164 | 165 | - **button:** add icon prop to button ([62453c1](https://github.com/estevanmaito/windmill-react-ui/commit/62453c19b9ba007fe40d634b9c9fa934220ca1bd)) 166 | - **dropdown:** add outside click handler ([c316cd4](https://github.com/estevanmaito/windmill-react-ui/commit/c316cd44ceadfc11be021308f977396eb282baba)) 167 | - **table:** remove styles from TableRow and apply to TableBody ([f5051c2](https://github.com/estevanmaito/windmill-react-ui/commit/f5051c216a3f776f09df85e397afb661cdaa97f5)) 168 | 169 | ### BREAKING CHANGES 170 | 171 | - **dropdown:** You don't need to provide an outside click handler to close the dropdown anymore as 172 | it's now done internally calling the passed onClose prop 173 | - **table:** TableRow could only be used inside TableBody because it would apply styles that 174 | would overwirte TableHead, like text colors. Now these styles live only inside TableBody, leaving 175 | TableRow without styles, even though it's entry is still in defaultTheme for further theme 176 | customization. 177 | 178 | # [0.1.0-alpha.13](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0-alpha.12...0.1.0-alpha.13) (2020-07-09) 179 | 180 | ### Bug Fixes 181 | 182 | - **form:** fix dark validation styles for input and textarea ([39af5d1](https://github.com/estevanmaito/windmill-react-ui/commit/39af5d1a9fc17c68e730f4ace177a9fcd62fa292)) 183 | 184 | ### Features 185 | 186 | - **select:** add disabled attribute ([d429e7a](https://github.com/estevanmaito/windmill-react-ui/commit/d429e7aa6ccbf0476509fe65ed3fb1f24024a613)) 187 | - **select:** add validation styles ([71d4dfe](https://github.com/estevanmaito/windmill-react-ui/commit/71d4dfe4f4c453ecb0be53ce5331f3183055c6e0)) 188 | - **textarea:** add validation and disabled styles ([6108c06](https://github.com/estevanmaito/windmill-react-ui/commit/6108c06584f4f3a91af587aee4963b3e50021abd)) 189 | 190 | # [0.1.0-alpha.12](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0-alpha.11...0.1.0-alpha.12) (2020-07-08) 191 | 192 | ### Bug Fixes 193 | 194 | - **modal:** do not render modal if it is not on client ([2cbe55a](https://github.com/estevanmaito/windmill-react-ui/commit/2cbe55a61862456c650d1c2bc4f30f8874dabd67)) 195 | - **plugin:** use opacity from theme for backgroundOpacity ([1c2931f](https://github.com/estevanmaito/windmill-react-ui/commit/1c2931f018f8f3af4ab0f84220aebfbc70af93bb)) 196 | 197 | # [0.1.0-alpha.11](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0-alpha.10...0.1.0-alpha.11) (2020-07-08) 198 | 199 | ### Bug Fixes 200 | 201 | - **badge:** add flex styles to badge ([bf7c412](https://github.com/estevanmaito/windmill-react-ui/commit/bf7c412c0baf28079f8faeb143251f3efc0f904e)) 202 | - **plugin:** fix backgroundOpacity variant overwrite ([d843446](https://github.com/estevanmaito/windmill-react-ui/commit/d84344612bf7f3dec6ed3dbcb6feeb789d787769)) 203 | 204 | ### BREAKING CHANGES 205 | 206 | - **badge:** it now behaves as inline-flex instead of default inline span. py-1 was also removed 207 | in favor of leading increase, so it better aligns in the center of the pill. 208 | 209 | # [0.1.0-alpha.10](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0-alpha.9...0.1.0-alpha.10) (2020-07-06) 210 | 211 | ### Bug Fixes 212 | 213 | - **plugin:** fix call stack on backgroundOpacity ([a9b9038](https://github.com/estevanmaito/windmill-react-ui/commit/a9b90380c8d349417f3c190abd00f2b36985c06c)) 214 | 215 | # [0.1.0-alpha.9](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0-alpha.8...0.1.0-alpha.9) (2020-07-06) 216 | 217 | ### Bug Fixes 218 | 219 | - **avatar:** add inline-block to avatar ([c1d4dc1](https://github.com/estevanmaito/windmill-react-ui/commit/c1d4dc115e6e9aa2a74c376f769eb50e284da524)) 220 | - **backdrop:** increase backdrop z-index ([951c489](https://github.com/estevanmaito/windmill-react-ui/commit/951c48946d663d1b9071b3d2073bcfcb8a06882a)) 221 | - **button:** fix link style for dark buttons ([1002423](https://github.com/estevanmaito/windmill-react-ui/commit/1002423618d29ef8cab320b629c762f3d2b7d408)), closes [#4](https://github.com/estevanmaito/windmill-react-ui/issues/4) 222 | - **dark mode:** fix window not being available on SSR ([af2d464](https://github.com/estevanmaito/windmill-react-ui/commit/af2d46409ed719a7103f50d8915440082fd02787)) 223 | 224 | ### Code Refactoring 225 | 226 | - **avatar:** use src instead of img ([e988424](https://github.com/estevanmaito/windmill-react-ui/commit/e98842497c588b5bd33b4feedd5ad6beb8d9f771)) 227 | 228 | ### BREAKING CHANGES 229 | 230 | - **avatar:** Avatar now uses src instead of img to pass the image src 231 | - **avatar:** add inline-block 232 | - **button:** different colors for dark mode 233 | - **backdrop:** z-index increased from 10 to 50 234 | 235 | # [0.1.0-alpha.8](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0-alpha.7...0.1.0-alpha.8) (2020-06-30) 236 | 237 | ### Bug Fixes 238 | 239 | - **defaulttheme:** export defaultTheme as Common JS and remove deprecated theme toggler ([d60e5c2](https://github.com/estevanmaito/windmill-react-ui/commit/d60e5c24d75206e753770630dce074cf028146dc)) 240 | - **table pagination:** remove layout styles from table footer that were interfering with pagination ([f9e7b9d](https://github.com/estevanmaito/windmill-react-ui/commit/f9e7b9d3b53d415cc0c02f9376856f4c6564716a)) 241 | 242 | # [0.1.0-alpha.7](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0-alpha.6...0.1.0-alpha.7) (2020-06-30) 243 | 244 | ### Bug Fixes 245 | 246 | - **windmill:** improve theme handling when user have stored theme ([d71b6df](https://github.com/estevanmaito/windmill-react-ui/commit/d71b6dfcebb2e78d2c72d1956d8d2d0e0bd3786c)) 247 | 248 | # [0.1.0-alpha.6](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0-alpha.5...0.1.0-alpha.6) (2020-06-29) 249 | 250 | ### Features 251 | 252 | - **windmill:** add theme storage and user system preferences getter as default ([04c06ef](https://github.com/estevanmaito/windmill-react-ui/commit/04c06ef1888a5675a5f05b784996b6ea9e8c4133)) 253 | 254 | # [0.1.0-alpha.5](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0-alpha.4...0.1.0-alpha.5) (2020-06-29) 255 | 256 | ### Bug Fixes 257 | 258 | - **backdrop:** reduce backdrop z-index ([732c546](https://github.com/estevanmaito/windmill-react-ui/commit/732c5462fb1b2a8ae890a0d62347c033ae0b39d0)) 259 | 260 | ### Features 261 | 262 | - **windmill:** expose current selected theme and a theme toggler ([73505bf](https://github.com/estevanmaito/windmill-react-ui/commit/73505bf8c9f8c55e68c2b4a7300bae91e4f3217c)) 263 | 264 | # [0.1.0-alpha.4](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0-alpha.3...0.1.0-alpha.4) (2020-06-28) 265 | 266 | ### Bug Fixes 267 | 268 | - **dropdown:** export dropdown item ([c177442](https://github.com/estevanmaito/windmill-react-ui/commit/c177442666aa4cb94b4b57c1d668691ed0416c5a)) 269 | 270 | # [0.1.0-alpha.3](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0-alpha.2...0.1.0-alpha.3) (2020-06-27) 271 | 272 | ### Bug Fixes 273 | 274 | - **library:** export TableFooter ([04ac997](https://github.com/estevanmaito/windmill-react-ui/commit/04ac9971c083dcd20ac2c9e0922b5d61878abc87)) 275 | 276 | # [0.1.0-alpha.2](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0-alpha.1...0.1.0-alpha.2) (2020-06-27) 277 | 278 | ### Bug Fixes 279 | 280 | - **webpack:** fix externals namingfix Module not found: Can't resolve 'reactDOM' ([380843b](https://github.com/estevanmaito/windmill-react-ui/commit/380843b5017a5eeeaf2a51e2b0ba620b2101a04b)) 281 | 282 | # [0.1.0-alpha.1](https://github.com/estevanmaito/windmill-react-ui/compare/0.1.0-alpha.1...0.1.0-alpha.2) (2020-06-27) 283 | 284 | ### Bug Fixes 285 | 286 | - **proptypes:** fix children proptypes ([43cdf46](https://github.com/estevanmaito/windmill-react-ui/commit/43cdf467c732d3d016fb19823558f97add801b2b)) 287 | - **windmill:** export windmill ([9983010](https://github.com/estevanmaito/windmill-react-ui/commit/99830107e66706f583f8552ef1d63a8b09afd337)) 288 | - **windmill:** fix dark theme prop ([7155eae](https://github.com/estevanmaito/windmill-react-ui/commit/7155eaea80411e2ad5fb04bd9e35c41c3a8b1cc8)) 289 | 290 | ### Features 291 | 292 | - **avatar:** add avatar ([ea7827b](https://github.com/estevanmaito/windmill-react-ui/commit/ea7827b41ec89466f31429f0759b93eb5935c09a)) 293 | - **backdrop:** add backdrop ([b367072](https://github.com/estevanmaito/windmill-react-ui/commit/b36707228f918e303e2457fcbc561302e2ec468f)) 294 | - **badge:** add badge ([7638177](https://github.com/estevanmaito/windmill-react-ui/commit/76381770e2db0d82dfa27fe657847baacc5a4c91)) 295 | - **card:** add card and cardbody ([3c64e85](https://github.com/estevanmaito/windmill-react-ui/commit/3c64e852be893871b346e9d2e2eb9797b0b90b87)) 296 | - **dropdown:** add dropdown ([e4eeba9](https://github.com/estevanmaito/windmill-react-ui/commit/e4eeba9715c2af0aa835097440b68171372b9095)) 297 | - **helpertext:** add helpertext ([12539d2](https://github.com/estevanmaito/windmill-react-ui/commit/12539d23809a42d77610f507495543e7518ec1ba)) 298 | - **input:** add form inputs ([8b4f69e](https://github.com/estevanmaito/windmill-react-ui/commit/8b4f69e1d26350312b0173551968ef88856fc468)) 299 | - **label:** add form label ([a74f8a9](https://github.com/estevanmaito/windmill-react-ui/commit/a74f8a978bd347de270bc8e4862febfaf3550b18)) 300 | - **modal:** add all modal components ([fc987d5](https://github.com/estevanmaito/windmill-react-ui/commit/fc987d57a0135631af627d332268f6e8b39ac88e)) 301 | - **pagination:** add pagination ([a410ca4](https://github.com/estevanmaito/windmill-react-ui/commit/a410ca4d572d5b2b2ed87f03de52ab7aaee554f0)) 302 | - **plugin:** add pluginexport a plugin that packs multi-theme, custom-forms and the custom shadow outline plugin, alongwith custom modifications to width, height and colors. ([15c772a](https://github.com/estevanmaito/windmill-react-ui/commit/15c772a1167dd61e94d185255b2f338b70b43672)) 303 | - **select:** add select ([da44e48](https://github.com/estevanmaito/windmill-react-ui/commit/da44e4882b5e90c997e6a25908a2697eac253283)) 304 | - **table:** add table ([6705f3d](https://github.com/estevanmaito/windmill-react-ui/commit/6705f3d4d9ab5d93833ead9fc9a7a73cbebd2951)) 305 | - **table:** add table footer ([19f59a8](https://github.com/estevanmaito/windmill-react-ui/commit/19f59a878d61d45b5ca6e8cd4ea0dfc0bc7e554d)) 306 | - **textarea:** add textarea ([6a7a333](https://github.com/estevanmaito/windmill-react-ui/commit/6a7a333db0c39ea58d7ec16bf0b6b47373afc014)) 307 | - **theme:** add default theme ([b99470c](https://github.com/estevanmaito/windmill-react-ui/commit/b99470cc690d2ea084cab2703391988c70e45ad7)) 308 | - **theme:** add theme context ([d3f13e7](https://github.com/estevanmaito/windmill-react-ui/commit/d3f13e7cc422f062006e3578030b6305e70f678d)) 309 | - **themes:** add dark mode and toggle ([371578e](https://github.com/estevanmaito/windmill-react-ui/commit/371578ee1109a8ca78c0fac6c4b8a509f5abdd2c)) 310 | - **themes:** expose themes configs ([f388a54](https://github.com/estevanmaito/windmill-react-ui/commit/f388a542ed2645b5edf8613283ab229d287c7ae3)) 311 | - **transition:** add transition ([46a7d6b](https://github.com/estevanmaito/windmill-react-ui/commit/46a7d6bee3a26cfe04dc68f51253f429465eb1f1)) 312 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [INSERT CONTACT]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Estevan Maito 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Windmill React UI 2 | 3 | The component library for fast and accessible development of gorgeous interfaces. 4 | 5 |

6 | codecov 7 | Build 8 | npm 9 | MIT License 10 |

11 | 12 | Projects using it: [Windmill Dashboard React](https://github.com/estevanmaito/windmill-dashboard-react) 13 | 14 | ## Mission 15 | 16 | Be the most accessible it can be out of the box and the fastest way to production. 17 | 18 | [Go to docs to see complete, live examples](https://windmillui.com/react-ui) 19 | 20 | ## 🚀 Usage 21 | 22 | Install 23 | 24 | ```sh 25 | npm i @windmill/react-ui 26 | ``` 27 | 28 | Inside `tailwind.config.js` 29 | 30 | ```js 31 | const windmill = require('@windmill/react-ui/config') 32 | module.exports = windmill({ 33 | purge: [], 34 | theme: { 35 | extend: {}, 36 | }, 37 | variants: {}, 38 | plugins: [], 39 | }) 40 | ``` 41 | 42 | Then place `Windmill` at the root of your project (the order doesn't matter, as long as your application is inside). 43 | 44 | ```js 45 | // index.js 46 | import React from 'react' 47 | import ReactDOM from 'react-dom' 48 | import App from './App' 49 | import { Windmill } from '@windmill/react-ui' 50 | 51 | ReactDOM.render( 52 | 53 | 54 | , 55 | document.getElementById('root') 56 | ) 57 | ``` 58 | 59 | Use components inside your project 60 | 61 | ```js 62 | import { Button } from '@windmill/react-ui' 63 | 64 | function App() { 65 | return 66 | } 67 | 68 | export default App 69 | ``` 70 | 71 | ## 🔌 Contributing 72 | 73 | - Fork 74 | - Clone 75 | - `npm install` 76 | - `npm run storybook` 77 | 78 | It will start a local server at `localhost:6006` with all components rendered. 79 | 80 | ⚠ Use `npm run cz` instead of `git commit`! It will guide you through some short questions and guarantee that you commit message is standardized. 81 | 82 | Commit will also trigger linting and test coverage. 83 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const deepMerge = require('deepmerge') 2 | const customFormsPlugin = require('@tailwindcss/forms') 3 | 4 | const colors = { 5 | transparent: 'transparent', 6 | white: '#ffffff', 7 | black: '#000000', 8 | gray: { 9 | '50': '#f9fafb', 10 | '100': '#f4f5f7', 11 | '200': '#e5e7eb', 12 | '300': '#d5d6d7', 13 | '400': '#9e9e9e', 14 | '500': '#707275', 15 | '600': '#4c4f52', 16 | '700': '#24262d', 17 | '800': '#1a1c23', 18 | '900': '#121317', 19 | // default values from Tailwind UI palette 20 | // '300': '#d2d6dc', 21 | // '400': '#9fa6b2', 22 | // '500': '#6b7280', 23 | // '600': '#4b5563', 24 | // '700': '#374151', 25 | // '800': '#252f3f', 26 | // '900': '#161e2e', 27 | }, 28 | 'cool-gray': { 29 | '50': '#fbfdfe', 30 | '100': '#f1f5f9', 31 | '200': '#e2e8f0', 32 | '300': '#cfd8e3', 33 | '400': '#97a6ba', 34 | '500': '#64748b', 35 | '600': '#475569', 36 | '700': '#364152', 37 | '800': '#27303f', 38 | '900': '#1a202e', 39 | }, 40 | red: { 41 | '50': '#fdf2f2', 42 | '100': '#fde8e8', 43 | '200': '#fbd5d5', 44 | '300': '#f8b4b4', 45 | '400': '#f98080', 46 | '500': '#f05252', 47 | '600': '#e02424', 48 | '700': '#c81e1e', 49 | '800': '#9b1c1c', 50 | '900': '#771d1d', 51 | }, 52 | orange: { 53 | '50': '#fff8f1', 54 | '100': '#feecdc', 55 | '200': '#fcd9bd', 56 | '300': '#fdba8c', 57 | '400': '#ff8a4c', 58 | '500': '#ff5a1f', 59 | '600': '#d03801', 60 | '700': '#b43403', 61 | '800': '#8a2c0d', 62 | '900': '#771d1d', 63 | }, 64 | yellow: { 65 | '50': '#fdfdea', 66 | '100': '#fdf6b2', 67 | '200': '#fce96a', 68 | '300': '#faca15', 69 | '400': '#e3a008', 70 | '500': '#c27803', 71 | '600': '#9f580a', 72 | '700': '#8e4b10', 73 | '800': '#723b13', 74 | '900': '#633112', 75 | }, 76 | green: { 77 | '50': '#f3faf7', 78 | '100': '#def7ec', 79 | '200': '#bcf0da', 80 | '300': '#84e1bc', 81 | '400': '#31c48d', 82 | '500': '#0e9f6e', 83 | '600': '#057a55', 84 | '700': '#046c4e', 85 | '800': '#03543f', 86 | '900': '#014737', 87 | }, 88 | teal: { 89 | '50': '#edfafa', 90 | '100': '#d5f5f6', 91 | '200': '#afecef', 92 | '300': '#7edce2', 93 | '400': '#16bdca', 94 | '500': '#0694a2', 95 | '600': '#047481', 96 | '700': '#036672', 97 | '800': '#05505c', 98 | '900': '#014451', 99 | }, 100 | blue: { 101 | '50': '#ebf5ff', 102 | '100': '#e1effe', 103 | '200': '#c3ddfd', 104 | '300': '#a4cafe', 105 | '400': '#76a9fa', 106 | '500': '#3f83f8', 107 | '600': '#1c64f2', 108 | '700': '#1a56db', 109 | '800': '#1e429f', 110 | '900': '#233876', 111 | }, 112 | indigo: { 113 | '50': '#f0f5ff', 114 | '100': '#e5edff', 115 | '200': '#cddbfe', 116 | '300': '#b4c6fc', 117 | '400': '#8da2fb', 118 | '500': '#6875f5', 119 | '600': '#5850ec', 120 | '700': '#5145cd', 121 | '800': '#42389d', 122 | '900': '#362f78', 123 | }, 124 | purple: { 125 | '50': '#f6f5ff', 126 | '100': '#edebfe', 127 | '200': '#dcd7fe', 128 | '300': '#cabffd', 129 | '400': '#ac94fa', 130 | '500': '#9061f9', 131 | '600': '#7e3af2', 132 | '700': '#6c2bd9', 133 | '800': '#5521b5', 134 | '900': '#4a1d96', 135 | }, 136 | pink: { 137 | '50': '#fdf2f8', 138 | '100': '#fce8f3', 139 | '200': '#fad1e8', 140 | '300': '#f8b4d9', 141 | '400': '#f17eb8', 142 | '500': '#e74694', 143 | '600': '#d61f69', 144 | '700': '#bf125d', 145 | '800': '#99154b', 146 | '900': '#751a3d', 147 | }, 148 | } 149 | 150 | const backgroundOpacity = (theme) => ({ 151 | '10': '0.1', 152 | ...theme('opacity'), 153 | }) 154 | 155 | const maxHeight = (theme) => ({ 156 | '0': '0', 157 | xl: '36rem', 158 | ...theme('spacing'), 159 | }) 160 | 161 | const windmillConfig = { 162 | darkMode: 'class', 163 | purge: { 164 | content: [ 165 | 'node_modules/@windmill/react-ui/lib/defaultTheme.js', 166 | 'node_modules/@windmill/react-ui/dist/index.js', 167 | ], 168 | }, 169 | theme: { 170 | colors, 171 | backgroundOpacity, 172 | maxHeight, 173 | }, 174 | variants: { 175 | backgroundOpacity: ['responsive', 'hover', 'focus', 'dark'], 176 | backgroundColor: ['responsive', 'hover', 'focus', 'active', 'odd', 'dark'], 177 | display: ['responsive', 'dark'], 178 | textColor: ['responsive', 'focus', 'focus-within', 'hover', 'active', 'dark'], 179 | placeholderColor: ['responsive', 'focus', 'dark'], 180 | borderColor: ['responsive', 'hover', 'focus', 'dark'], 181 | divideColor: ['responsive', 'dark'], 182 | boxShadow: ['responsive', 'hover', 'focus', 'dark'], 183 | margin: ['responsive', 'last'], 184 | }, 185 | plugins: [customFormsPlugin], 186 | } 187 | 188 | function arrayMergeFn(destinationArray, sourceArray) { 189 | return destinationArray.concat(sourceArray).reduce((acc, cur) => { 190 | if (acc.includes(cur)) return acc 191 | return [...acc, cur] 192 | }, []) 193 | } 194 | 195 | /** 196 | * Merge Windmill and Tailwind CSS configurations 197 | * @param {object} tailwindConfig - Tailwind config object 198 | * @return {object} new config object 199 | */ 200 | function wrapper(tailwindConfig) { 201 | let purge 202 | if (Array.isArray(tailwindConfig.purge)) { 203 | purge = { 204 | content: tailwindConfig.purge, 205 | } 206 | } else { 207 | purge = tailwindConfig.purge 208 | } 209 | return deepMerge({ ...tailwindConfig, purge }, windmillConfig, { arrayMerge: arrayMergeFn }) 210 | } 211 | 212 | module.exports = wrapper 213 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@windmill/react-ui", 3 | "version": "0.6.0", 4 | "description": "React UI component library built with Tailwind CSS", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "lib", 8 | "dist", 9 | "config.js", 10 | "README.md" 11 | ], 12 | "scripts": { 13 | "prebuild": "rimraf dist lib && npm run build:lib && npm run build:ts", 14 | "build:ts": "tsc --declaration --declarationMap --emitDeclarationOnly", 15 | "build:tailwind": "tailwindcss build style/tailwind.css -o style/output.css -c style/tailwind.config.js", 16 | "build:lib": "babel src/themes/default.ts --out-file lib/defaultTheme.js", 17 | "build": "webpack", 18 | "predev": "npm run build:tailwind", 19 | "cz": "git-cz", 20 | "test": "jest", 21 | "codecov": "codecov", 22 | "test:watch": "jest --watch", 23 | "test:coverage": "jest --coverage", 24 | "lint": "eslint .", 25 | "lint:fix": "eslint . --fix", 26 | "prerelease": "npm run build", 27 | "release": "release-it", 28 | "prestorybook": "npm run build:tailwind", 29 | "storybook": "start-storybook -p 6006", 30 | "build-storybook": "build-storybook" 31 | }, 32 | "peerDependencies": { 33 | "react": ">=16.8.0", 34 | "react-dom": ">=16.8.0", 35 | "tailwindcss": ">=2.0.0" 36 | }, 37 | "dependencies": { 38 | "@tailwindcss/forms": "^0.3.2", 39 | "classnames": "2.2.6", 40 | "deepmerge": "4.2.2", 41 | "postcss": "^8.2.15", 42 | "react-focus-lock": "2.4.1", 43 | "react-transition-group": "4.4.1" 44 | }, 45 | "devDependencies": { 46 | "@babel/cli": "^7.8.4", 47 | "@babel/core": "^7.9.6", 48 | "@babel/preset-env": "^7.9.6", 49 | "@babel/preset-react": "^7.10.1", 50 | "@babel/preset-typescript": "^7.10.4", 51 | "@release-it/conventional-changelog": "^2.0.0", 52 | "@storybook/addon-actions": "^6.2.9", 53 | "@storybook/addon-essentials": "^6.2.9", 54 | "@storybook/addon-links": "^6.2.9", 55 | "@storybook/react": "^6.2.9", 56 | "@svgr/webpack": "^5.5.0", 57 | "@types/classnames": "^2.2.10", 58 | "@types/enzyme": "^3.10.5", 59 | "@types/jest": "^26.0.13", 60 | "@types/react": "^16.9.49", 61 | "@types/react-dom": "^16.9.8", 62 | "@types/react-transition-group": "4.4.0", 63 | "@wojtekmaj/enzyme-adapter-react-17": "0.3.2", 64 | "babel-jest": "^26.1.0", 65 | "babel-loader": "^8.1.0", 66 | "codecov": "^3.7.1", 67 | "commitizen": "^4.2.4", 68 | "css-loader": "^3.6.0", 69 | "cz-conventional-changelog": "^3.2.0", 70 | "enzyme": "^3.11.0", 71 | "eslint": "^7.0.0", 72 | "eslint-config-prettier": "^6.11.0", 73 | "eslint-config-standard": "^14.1.1", 74 | "eslint-plugin-import": "^2.20.2", 75 | "eslint-plugin-node": "^11.1.0", 76 | "eslint-plugin-prettier": "^3.1.3", 77 | "eslint-plugin-promise": "^4.2.1", 78 | "eslint-plugin-react": "^7.20.0", 79 | "eslint-plugin-standard": "^4.0.1", 80 | "html-webpack-plugin": "^5.3.1", 81 | "husky": "^4.2.5", 82 | "jest": "^26.0.1", 83 | "jest-svg-transformer": "^1.0.0", 84 | "prettier": "^2.0.5", 85 | "react": "^17.0.1", 86 | "react-dom": "^17.0.1", 87 | "react-is": "^17.0.1", 88 | "release-it": "^14.6.1", 89 | "rimraf": "^3.0.2", 90 | "style-loader": "^1.2.1", 91 | "tailwindcss": "^2.0.0", 92 | "ts-jest": "^26.3.0", 93 | "ts-loader": "^8.0.3", 94 | "typescript": "^4.0.2", 95 | "webpack": "^5.1.3", 96 | "webpack-bundle-analyzer": "^3.8.0", 97 | "webpack-cli": "^4.1.0" 98 | }, 99 | "repository": { 100 | "type": "git", 101 | "url": "https://github.com/estevanmaito/windmill-react-ui.git" 102 | }, 103 | "keywords": [], 104 | "author": "Estevan Maito ", 105 | "license": "MIT", 106 | "bugs": { 107 | "url": "https://github.com/estevanmaito/windmill-react-ui/issues" 108 | }, 109 | "homepage": "https://github.com/estevanmaito/windmill-react-ui#readme", 110 | "release-it": { 111 | "github": { 112 | "release": true 113 | }, 114 | "plugins": { 115 | "@release-it/conventional-changelog": { 116 | "preset": "angular", 117 | "infile": "CHANGELOG.md" 118 | } 119 | } 120 | }, 121 | "publishConfig": { 122 | "access": "public" 123 | }, 124 | "babel": { 125 | "presets": [ 126 | "@babel/preset-env", 127 | "@babel/preset-react" 128 | ] 129 | }, 130 | "prettier": { 131 | "printWidth": 100, 132 | "semi": false, 133 | "singleQuote": true, 134 | "trailingComma": "es5", 135 | "endOfLine": "lf" 136 | }, 137 | "jest": { 138 | "preset": "ts-jest", 139 | "testPathIgnorePatterns": [ 140 | ".*\\.d\\.ts", 141 | "/node_modules/" 142 | ], 143 | "collectCoverageFrom": [ 144 | "src/**/*.{ts,tsx}", 145 | "!src/{Transition,index}.{ts,tsx}", 146 | "!src/{stories,utils}/*" 147 | ], 148 | "setupFilesAfterEnv": [ 149 | "./setupTests.ts" 150 | ], 151 | "coverageThreshold": { 152 | "global": { 153 | "branches": 100, 154 | "functions": 100, 155 | "lines": 100, 156 | "statements": 100 157 | } 158 | }, 159 | "transform": { 160 | "^.+\\.svg$": "jest-svg-transformer", 161 | "^.+\\.(ts|tsx)$": "ts-jest" 162 | } 163 | }, 164 | "config": { 165 | "commitizen": { 166 | "path": "./node_modules/cz-conventional-changelog" 167 | } 168 | }, 169 | "husky": { 170 | "hooks": { 171 | "pre-commit": "npm run lint && npm run test:coverage" 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /setupTests.ts: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme' 2 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17' 3 | 4 | configure({ adapter: new Adapter() }) 5 | -------------------------------------------------------------------------------- /src/Alert.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGAttributes, useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | enum AlertEnum { 6 | success, 7 | danger, 8 | warning, 9 | info, 10 | neutral, 11 | } 12 | 13 | export interface AlertProps extends React.HTMLAttributes { 14 | /** 15 | * The type of the alert 16 | */ 17 | type?: keyof typeof AlertEnum 18 | /** 19 | * If defined, shows the close icon that calls this function 20 | */ 21 | onClose?: () => void 22 | } 23 | 24 | type IconProps = SVGAttributes 25 | 26 | export const InfoIcon: React.FC = (props) => ( 27 | 36 | 37 | 38 | ) 39 | 40 | export const WarningIcon: React.FC = (props) => ( 41 | 50 | 51 | 52 | ) 53 | 54 | export const DangerIcon: React.FC = (props) => ( 55 | 64 | 65 | 66 | ) 67 | 68 | export const SuccessIcon: React.FC = (props) => ( 69 | 78 | 79 | 80 | ) 81 | 82 | export const NeutralIcon: React.FC = (props) => ( 83 | 92 | 93 | 94 | ) 95 | 96 | const Alert = React.forwardRef(function Alert(props, ref) { 97 | const { className, children, type = 'neutral', onClose, ...other } = props 98 | const { 99 | theme: { alert }, 100 | } = useContext(ThemeContext) 101 | 102 | const baseStyle = alert.base 103 | const withCloseStyle = alert.withClose 104 | const typeStyle = alert[type] 105 | const iconBaseStyle = alert.icon.base 106 | const iconTypeStyle = alert.icon[type] 107 | 108 | let Icon 109 | switch (type) { 110 | case 'success': 111 | Icon = SuccessIcon 112 | break 113 | case 'warning': 114 | Icon = WarningIcon 115 | break 116 | case 'danger': 117 | Icon = DangerIcon 118 | break 119 | case 'info': 120 | Icon = InfoIcon 121 | break 122 | case 'neutral': 123 | Icon = NeutralIcon 124 | break 125 | default: 126 | Icon = NeutralIcon 127 | } 128 | 129 | const cls = classNames(baseStyle, typeStyle, onClose && withCloseStyle, className) 130 | 131 | const iconCls = classNames(iconBaseStyle, iconTypeStyle, 'absolute left-0 top-0 ml-4 mt-4') 132 | const closeCls = classNames(iconBaseStyle, iconTypeStyle) 133 | 134 | return ( 135 |
136 | {onClose && ( 137 | 152 | )} 153 | 154 | {children} 155 |
156 | ) 157 | }) 158 | 159 | export default Alert 160 | -------------------------------------------------------------------------------- /src/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | export interface AvatarProps extends React.HTMLAttributes { 6 | /** 7 | * The size of the avatar 8 | */ 9 | size?: 'large' | 'regular' | 'small' 10 | /** 11 | * Alternative text for the avatar image 12 | */ 13 | alt?: string 14 | /** 15 | * The source for the avatar image 16 | */ 17 | src: string 18 | } 19 | 20 | const Avatar = React.forwardRef(function Avatar(props, ref) { 21 | const { size = 'regular', src, alt, className, ...other } = props 22 | const { 23 | theme: { avatar }, 24 | } = useContext(ThemeContext) 25 | 26 | const baseStyle = avatar.base 27 | const sizeStyles = { 28 | large: avatar.size.large, 29 | regular: avatar.size.regular, 30 | small: avatar.size.small, 31 | } 32 | 33 | const cls = classNames(baseStyle, sizeStyles[size], className) 34 | 35 | return ( 36 |
37 | {alt} 38 | 39 |
40 | ) 41 | }) 42 | 43 | export default Avatar 44 | -------------------------------------------------------------------------------- /src/Backdrop.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | export interface BackdropProps extends React.HTMLAttributes {} 6 | 7 | const Backdrop = React.forwardRef(function Backdrop(props, ref) { 8 | const { className, ...other } = props 9 | const { 10 | theme: { backdrop }, 11 | } = useContext(ThemeContext) 12 | 13 | const baseStyle = backdrop.base 14 | 15 | const cls = classNames(baseStyle, className) 16 | return
17 | }) 18 | 19 | export default Backdrop 20 | -------------------------------------------------------------------------------- /src/Badge.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | export interface BadgeProps extends React.HTMLAttributes { 6 | /** 7 | * The type of the badge 8 | */ 9 | type?: 'success' | 'danger' | 'warning' | 'neutral' | 'primary' 10 | } 11 | 12 | const Badge = React.forwardRef(function Badge(props, ref) { 13 | const { className, children, type = 'primary', ...other } = props 14 | 15 | const { 16 | theme: { badge }, 17 | } = useContext(ThemeContext) 18 | 19 | const baseStyle = badge.base 20 | const typeStyle = { 21 | success: badge.success, 22 | danger: badge.danger, 23 | warning: badge.warning, 24 | neutral: badge.neutral, 25 | primary: badge.primary, 26 | } 27 | 28 | const cls = classNames(baseStyle, typeStyle[type], className) 29 | 30 | return ( 31 | 32 | {children} 33 | 34 | ) 35 | }) 36 | 37 | export default Badge 38 | -------------------------------------------------------------------------------- /src/Button.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames' 2 | import React, { ReactNode, useContext } from 'react' 3 | import { ThemeContext } from './context/ThemeContext' 4 | import warn from './utils/warning' 5 | 6 | type IconType = 7 | | string 8 | | React.FunctionComponent<{ className: string; 'aria-hidden': boolean }> 9 | | React.ComponentClass<{ className: string; 'aria-hidden': boolean }> 10 | 11 | export interface Props { 12 | children?: React.ReactNode 13 | /** 14 | * Defines if the button is disabled 15 | */ 16 | disabled?: boolean 17 | /** 18 | * The size of the button 19 | */ 20 | size?: 'larger' | 'large' | 'regular' | 'small' | 'pagination' 21 | /** 22 | * Shows only one icon inside the button; defaults to left 23 | */ 24 | icon?: IconType 25 | /** 26 | * Shows an icon inside the button, left aligned 27 | */ 28 | iconLeft?: IconType 29 | /** 30 | * Shows an icon inside the button, right aligned 31 | */ 32 | iconRight?: IconType 33 | /** 34 | * The style of the button 35 | */ 36 | layout?: 'outline' | 'link' | 'primary' | '__dropdownItem' 37 | /** 38 | * Shows the button as a block (full width) 39 | */ 40 | block?: boolean 41 | } 42 | 43 | export interface ButtonAsButtonProps extends Props, React.ButtonHTMLAttributes { 44 | /** 45 | * The element that should be rendered as a button 46 | */ 47 | tag?: 'button' 48 | /** 49 | * The native HTML button type 50 | */ 51 | type?: 'button' | 'submit' | 'reset' 52 | } 53 | 54 | export interface ButtonAsAnchorProps extends Props, React.AnchorHTMLAttributes { 55 | tag: 'a' 56 | } 57 | 58 | export interface ButtonAsOtherProps extends Props, React.AnchorHTMLAttributes { 59 | tag: string 60 | } 61 | 62 | export type ButtonProps = ButtonAsButtonProps | ButtonAsAnchorProps | ButtonAsOtherProps 63 | 64 | type Ref = ReactNode | HTMLElement | string 65 | 66 | const Button = React.forwardRef(function Button(props, ref) { 67 | const { 68 | tag = 'button', 69 | // Fix https://github.com/estevanmaito/windmill-react-ui/issues/7 70 | type = tag === 'button' ? 'button' : undefined, 71 | disabled = false, 72 | size = 'regular', 73 | layout = 'primary', 74 | block = false, 75 | icon, 76 | iconLeft, 77 | iconRight, 78 | className, 79 | children, 80 | ...other 81 | } = props 82 | const { 83 | theme: { button }, 84 | } = useContext(ThemeContext) 85 | 86 | function hasIcon() { 87 | return !!icon || !!iconLeft || !!iconRight 88 | } 89 | 90 | warn( 91 | hasIcon() && !other['aria-label'] && !children, 92 | 'Button', 93 | 'You are using an icon button, but no "aria-label" attribute was found. Add an "aria-label" attribute to work as a label for screen readers.' 94 | ) 95 | 96 | const IconLeft = iconLeft || icon 97 | const IconRight = iconRight 98 | 99 | const baseStyle = button.base 100 | const blockStyle = button.block 101 | const sizeStyles = { 102 | larger: button.size.larger, 103 | large: button.size.large, 104 | regular: button.size.regular, 105 | small: button.size.small, 106 | /** 107 | * Only used in Pagination. 108 | * Not meant for general use. 109 | */ 110 | pagination: button.size.pagination, 111 | } 112 | const iconSizeStyles = { 113 | larger: button.size.icon.larger, 114 | large: button.size.icon.large, 115 | regular: button.size.icon.regular, 116 | small: button.size.icon.small, 117 | pagination: button.size.icon.regular, 118 | } 119 | const iconStyle = button.icon[size] 120 | const layoutStyles = { 121 | primary: button.primary.base, 122 | outline: button.outline.base, 123 | link: button.link.base, 124 | } 125 | const activeStyles = { 126 | primary: button.primary.active, 127 | outline: button.outline.active, 128 | link: button.link.active, 129 | } 130 | const disabledStyles = { 131 | primary: button.primary.disabled, 132 | outline: button.outline.disabled, 133 | link: button.link.disabled, 134 | } 135 | 136 | /** 137 | * Only used in DropdownItem. 138 | * Not meant for general use. 139 | */ 140 | const dropdownItemStyle = button.dropdownItem.base 141 | 142 | const buttonStyles = 143 | layout === '__dropdownItem' 144 | ? classNames(dropdownItemStyle, className) 145 | : classNames( 146 | baseStyle, 147 | // has icon but no children 148 | hasIcon() && !children && iconSizeStyles[size], 149 | // has icon and children 150 | hasIcon() && children && sizeStyles[size], 151 | // does not have icon 152 | !hasIcon() && sizeStyles[size], 153 | layoutStyles[layout], 154 | disabled ? disabledStyles[layout] : activeStyles[layout], 155 | block ? blockStyle : null, 156 | className 157 | ) 158 | 159 | const iconLeftStyles = classNames(iconStyle, children ? button.icon.left : '') 160 | const iconRightStyles = classNames(iconStyle, children ? button.icon.right : '') 161 | 162 | return React.createElement( 163 | tag, 164 | { 165 | className: buttonStyles, 166 | ref, 167 | disabled, 168 | type, 169 | ...other, 170 | }, 171 | IconLeft 172 | ? React.createElement(IconLeft, { className: iconLeftStyles, 'aria-hidden': true }) 173 | : null, 174 | children, 175 | IconRight 176 | ? React.createElement(IconRight, { className: iconRightStyles, 'aria-hidden': true }) 177 | : null 178 | ) 179 | }) 180 | 181 | export default Button 182 | -------------------------------------------------------------------------------- /src/Card.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | export interface CardProps extends React.HTMLAttributes { 6 | /** 7 | * Removes default styles (if true) so you can override with your own background styles 8 | */ 9 | colored?: boolean 10 | } 11 | 12 | const Card = React.forwardRef(function Card(props, ref) { 13 | const { className, children, colored = false, ...other } = props 14 | const { 15 | theme: { card }, 16 | } = useContext(ThemeContext) 17 | 18 | const baseStyle = card.base 19 | const uncoloredStyle = card.default 20 | 21 | const cls = classNames(baseStyle, !colored && uncoloredStyle, className) 22 | 23 | return ( 24 |
25 | {children} 26 |
27 | ) 28 | }) 29 | 30 | export default Card 31 | -------------------------------------------------------------------------------- /src/CardBody.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | interface Props extends React.HTMLAttributes {} 6 | 7 | const CardBody = React.forwardRef(function CardBody(props, ref) { 8 | const { className, children, ...other } = props 9 | const { 10 | theme: { cardBody }, 11 | } = useContext(ThemeContext) 12 | 13 | const baseStyle = cardBody.base 14 | 15 | const cls = classNames(baseStyle, className) 16 | 17 | return ( 18 |
19 | {children} 20 |
21 | ) 22 | }) 23 | 24 | export default CardBody 25 | -------------------------------------------------------------------------------- /src/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext, useRef } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | import Transition from './Transition' 5 | import FocusLock from 'react-focus-lock' 6 | 7 | export interface DropdownProps extends React.HTMLAttributes { 8 | /** 9 | * Function executed when the dropdown is closed 10 | */ 11 | onClose: () => void 12 | /** 13 | * Defines if the dropdown is open 14 | */ 15 | isOpen: boolean 16 | /** 17 | * Defines the alignement of the dropdown related to its parent 18 | */ 19 | align?: 'left' | 'right' 20 | } 21 | 22 | const Dropdown = React.forwardRef(function Dropdown(props, ref) { 23 | const { children, onClose, isOpen, className, align = 'left', ...other } = props 24 | const { 25 | theme: { dropdown }, 26 | } = useContext(ThemeContext) 27 | 28 | const baseStyle = dropdown.base 29 | const alignStyle = dropdown.align[align] 30 | 31 | function handleEsc(e: KeyboardEvent) { 32 | if (e.key === 'Esc' || e.key === 'Escape') { 33 | onClose() 34 | } 35 | } 36 | 37 | const dropdownRef = useRef(null) 38 | function handleClickOutside(e: MouseEvent) { 39 | if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { 40 | onClose() 41 | } 42 | } 43 | 44 | useEffect(() => { 45 | document.addEventListener('click', handleClickOutside, { capture: true }) 46 | document.addEventListener('keydown', handleEsc, { capture: true }) 47 | return () => { 48 | document.removeEventListener('click', handleClickOutside) 49 | document.removeEventListener('keydown', handleEsc) 50 | } 51 | }, [isOpen]) 52 | 53 | const cls = classNames(baseStyle, alignStyle, className) 54 | 55 | return ( 56 | 62 |
63 | 64 |
    65 | {children} 66 |
67 |
68 |
69 |
70 | ) 71 | }) 72 | 73 | export default Dropdown 74 | -------------------------------------------------------------------------------- /src/DropdownItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { ButtonProps } from './Button' 3 | import Button from './Button' 4 | import { ThemeContext } from './context/ThemeContext' 5 | 6 | type Ref = typeof Button 7 | const DropdownItem = React.forwardRef(function DropdownItem(props, ref) { 8 | // Note: className is passed to the inner Button 9 | const { children, ...other } = props 10 | 11 | const { 12 | theme: { dropdownItem }, 13 | } = useContext(ThemeContext) 14 | 15 | const baseStyle = dropdownItem.base 16 | 17 | return ( 18 |
  • 19 | 22 |
  • 23 | ) 24 | }) 25 | 26 | export default DropdownItem 27 | -------------------------------------------------------------------------------- /src/HelperText.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | export interface HelperTextProps extends React.HTMLAttributes { 6 | /** 7 | * Defines the color of the helper text (the same as with Input, Select, etc.) 8 | */ 9 | valid?: boolean 10 | } 11 | 12 | const HelperText = React.forwardRef(function HelperText( 13 | props, 14 | ref 15 | ) { 16 | const { children, valid, className, ...other } = props 17 | const { 18 | theme: { helperText }, 19 | } = useContext(ThemeContext) 20 | 21 | const baseStyle = helperText.base 22 | const validStyle = helperText.valid 23 | const invalidStyle = helperText.invalid 24 | 25 | const validationStyle = (valid: boolean | undefined): string => { 26 | switch (valid) { 27 | case true: 28 | return validStyle 29 | case false: 30 | return invalidStyle 31 | default: 32 | return '' 33 | } 34 | } 35 | 36 | const cls = classNames(baseStyle, validationStyle(valid), className) 37 | 38 | return ( 39 | 40 | {children} 41 | 42 | ) 43 | }) 44 | 45 | export default HelperText 46 | -------------------------------------------------------------------------------- /src/Input.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | export interface InputProps extends React.ComponentPropsWithRef<'input'> { 6 | /** 7 | * Defines the color of the input 8 | */ 9 | valid?: boolean 10 | /** 11 | * Defines if the input is disabled 12 | */ 13 | disabled?: boolean 14 | /** 15 | * Defines the type of the input 16 | */ 17 | type?: string 18 | } 19 | 20 | const Input = React.forwardRef(function Input(props, ref) { 21 | const { valid, disabled, className, type = 'text', ...other } = props 22 | 23 | const { 24 | theme: { input }, 25 | } = useContext(ThemeContext) 26 | 27 | const baseStyle = input.base 28 | const activeStyle = input.active 29 | const disabledStyle = input.disabled 30 | const validStyle = input.valid 31 | const invalidStyle = input.invalid 32 | const radioStyle = input.radio 33 | const checkStyle = input.checkbox 34 | 35 | function hasValidation(valid: boolean | undefined) { 36 | return valid !== undefined 37 | } 38 | 39 | function validationStyle(valid: boolean | undefined): string { 40 | if (hasValidation(valid)) { 41 | return valid ? validStyle : invalidStyle 42 | } 43 | return '' 44 | } 45 | 46 | function typeStyle(type: string): string { 47 | switch (type) { 48 | case 'radio': 49 | return radioStyle 50 | case 'checkbox': 51 | return checkStyle 52 | default: 53 | return baseStyle 54 | } 55 | } 56 | 57 | const cls = classNames( 58 | typeStyle(type), 59 | // don't apply activeStyle if has valid or disabled 60 | !hasValidation(valid) && !disabled && activeStyle, 61 | // don't apply disabledStyle if has valid 62 | !hasValidation(valid) && disabled && disabledStyle, 63 | validationStyle(valid), 64 | className 65 | ) 66 | 67 | return 68 | }) 69 | 70 | export default Input 71 | -------------------------------------------------------------------------------- /src/Label.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | export interface LabelProps extends React.HTMLAttributes { 6 | /** 7 | * Applies specific styles for checkboxes 8 | */ 9 | check?: boolean 10 | /** 11 | * Applies specific styles for radios 12 | */ 13 | radio?: boolean 14 | /** 15 | * Defines if the label is disabled (you should still disable child elements) 16 | */ 17 | disabled?: boolean 18 | } 19 | 20 | const Label = React.forwardRef(function Label(props, ref) { 21 | const { children, check, radio, disabled, className, ...other } = props 22 | const { 23 | theme: { label }, 24 | } = useContext(ThemeContext) 25 | 26 | const baseStyle = label.base 27 | const checkStyle = label.check 28 | const disabledStyle = label.disabled 29 | 30 | const cls = classNames( 31 | baseStyle, 32 | // check and radio are interchangeable 33 | (check || radio) && checkStyle, 34 | disabled && disabledStyle, 35 | className 36 | ) 37 | 38 | return ( 39 | 42 | ) 43 | }) 44 | 45 | export default Label 46 | -------------------------------------------------------------------------------- /src/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext, useState } from 'react' 2 | import { createPortal } from 'react-dom' 3 | import Backdrop from './Backdrop' 4 | import Transition from './Transition' 5 | import FocusLock from 'react-focus-lock' 6 | 7 | import { ThemeContext } from './context/ThemeContext' 8 | 9 | export interface ModalProps extends React.HTMLAttributes { 10 | /** 11 | * Function executed when the dropdown is closed 12 | */ 13 | onClose: () => void 14 | /** 15 | * Defines if the modal is open 16 | */ 17 | isOpen: boolean 18 | } 19 | 20 | const Modal = React.forwardRef(function Modal(props, ref) { 21 | const { children, onClose, isOpen, ...other } = props 22 | 23 | const { 24 | theme: { modal }, 25 | } = useContext(ThemeContext) 26 | 27 | const baseStyle = modal.base 28 | 29 | function handleEsc(e: KeyboardEvent) { 30 | if (e.key === 'Esc' || e.key === 'Escape') { 31 | onClose() 32 | } 33 | } 34 | 35 | useEffect(() => { 36 | document.addEventListener('keydown', handleEsc, { capture: true }) 37 | return () => { 38 | document.removeEventListener('keydown', handleEsc) 39 | } 40 | }) 41 | 42 | const [mounted, setMounted] = useState(false) 43 | 44 | useEffect(() => { 45 | setMounted(true) 46 | }, []) 47 | 48 | const modalComponent = ( 49 | 50 | <> 51 | 59 | 60 | 68 |
    e.stopPropagation()} 72 | ref={ref} 73 | {...other} 74 | > 75 | 76 |
    77 | 96 |
    97 | {children} 98 |
    99 |
    100 |
    101 |
    102 |
    103 | 104 |
    105 | ) 106 | 107 | return mounted ? createPortal(modalComponent, document.body) : null 108 | }) 109 | 110 | export default Modal 111 | -------------------------------------------------------------------------------- /src/ModalBody.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | interface Props extends React.HTMLAttributes {} 6 | 7 | const ModalBody = React.forwardRef(function ModalBody(props, ref) { 8 | const { children, className, ...other } = props 9 | const { 10 | theme: { modalBody }, 11 | } = useContext(ThemeContext) 12 | 13 | const baseStyle = modalBody.base 14 | 15 | const cls = classNames(baseStyle, className) 16 | 17 | return ( 18 |
    19 | {children} 20 |
    21 | ) 22 | }) 23 | 24 | export default ModalBody 25 | -------------------------------------------------------------------------------- /src/ModalFooter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | interface Props extends React.HTMLAttributes {} 6 | 7 | const ModalFooter = React.forwardRef(function ModalFooter(props, ref) { 8 | const { children, className, ...other } = props 9 | const { 10 | theme: { modalFooter }, 11 | } = useContext(ThemeContext) 12 | 13 | const baseStyle = modalFooter.base 14 | 15 | const cls = classNames(baseStyle, className) 16 | 17 | return ( 18 |
    19 | {children} 20 |
    21 | ) 22 | }) 23 | 24 | export default ModalFooter 25 | -------------------------------------------------------------------------------- /src/ModalHeader.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | interface Props extends React.HTMLAttributes {} 6 | 7 | const ModalHeader = React.forwardRef(function ModalHeader(props, ref) { 8 | const { children, className, ...other } = props 9 | const { 10 | theme: { modalHeader }, 11 | } = useContext(ThemeContext) 12 | 13 | const baseStyle = modalHeader.base 14 | 15 | const cls = classNames(baseStyle, className) 16 | 17 | return ( 18 |

    19 | {children} 20 |

    21 | ) 22 | }) 23 | 24 | export default ModalHeader 25 | -------------------------------------------------------------------------------- /src/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react' 2 | import Button, { ButtonAsButtonProps } from './Button' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | const PrevIcon: React.FC = function PrevIcon(props) { 6 | return ( 7 | 14 | ) 15 | } 16 | 17 | const NextIcon: React.FC = function NextIcon(props) { 18 | return ( 19 | 26 | ) 27 | } 28 | 29 | interface NavigationButtonProps extends ButtonAsButtonProps { 30 | directionIcon: 'prev' | 'next' 31 | } 32 | 33 | export const NavigationButton: React.FC = function NavigationButton({ 34 | onClick, 35 | disabled, 36 | directionIcon, 37 | }) { 38 | const ariaLabel = directionIcon === 'prev' ? 'Previous' : 'Next' 39 | 40 | const icon = directionIcon === 'prev' ? PrevIcon : NextIcon 41 | 42 | return ( 43 | 74 | ) 75 | } 76 | 77 | export const EmptyPageButton = () => ... 78 | 79 | export interface PaginationProps { 80 | /** 81 | * The total number of results 82 | */ 83 | totalResults: number 84 | /** 85 | * The number of results shown per page 86 | */ 87 | resultsPerPage?: number 88 | /** 89 | * The accessible name of the pagination (what does it refer to?) 90 | */ 91 | label: string 92 | /** 93 | * The function executed on page change 94 | */ 95 | onChange: (activePage: number) => void 96 | } 97 | 98 | type Ref = HTMLDivElement 99 | 100 | const Pagination = React.forwardRef(function Pagination(props, ref) { 101 | const { totalResults, resultsPerPage = 10, label, onChange, ...other } = props 102 | const [pages, setPages] = useState<(number | string)[]>([]) 103 | const [activePage, setActivePage] = useState(1) 104 | 105 | const TOTAL_PAGES = Math.ceil(totalResults / resultsPerPage) 106 | const FIRST_PAGE = 1 107 | const LAST_PAGE = TOTAL_PAGES 108 | const MAX_VISIBLE_PAGES = 7 109 | 110 | function handlePreviousClick() { 111 | setActivePage(activePage - 1) 112 | } 113 | 114 | function handleNextClick() { 115 | setActivePage(activePage + 1) 116 | } 117 | 118 | useEffect(() => { 119 | // [1], 2, 3, 4, 5, ..., 12 case #1 120 | // 1, [2], 3, 4, 5, ..., 12 121 | // 1, 2, [3], 4, 5, ..., 12 122 | // 1, 2, 3, [4], 5, ..., 12 123 | // 1, ..., 4, [5], 6, ..., 12 case #2 124 | // 1, ..., 5, [6], 7, ..., 12 125 | // 1, ..., 6, [7], 8, ..., 12 126 | // 1, ..., 7, [8], 9, ..., 12 127 | // 1, ..., 8, [9], 10, 11, 12 case #3 128 | // 1, ..., 8, 9, [10], 11, 12 129 | // 1, ..., 8, 9, 10, [11], 12 130 | // 1, ..., 8, 9, 10, 11, [12] 131 | // [1], 2, 3, 4, 5, ..., 8 132 | // always show first and last 133 | // max of 7 pages shown (incl. [...]) 134 | if (TOTAL_PAGES <= MAX_VISIBLE_PAGES) { 135 | setPages(Array.from({ length: TOTAL_PAGES }).map((_, i) => i + 1)) 136 | } else if (activePage < 5) { 137 | // #1 active page < 5 -> show first 5 138 | setPages([1, 2, 3, 4, 5, '...', TOTAL_PAGES]) 139 | } else if (activePage >= 5 && activePage < TOTAL_PAGES - 3) { 140 | // #2 active page >= 5 && < TOTAL_PAGES - 3 141 | setPages([1, '...', activePage - 1, activePage, activePage + 1, '...', TOTAL_PAGES]) 142 | } else { 143 | // #3 active page >= TOTAL_PAGES - 3 -> show last 144 | setPages([ 145 | 1, 146 | '...', 147 | TOTAL_PAGES - 4, 148 | TOTAL_PAGES - 3, 149 | TOTAL_PAGES - 2, 150 | TOTAL_PAGES - 1, 151 | TOTAL_PAGES, 152 | ]) 153 | } 154 | }, [activePage, TOTAL_PAGES]) 155 | 156 | useEffect(() => { 157 | onChange(activePage) 158 | }, [activePage]) 159 | 160 | const { 161 | theme: { pagination }, 162 | } = useContext(ThemeContext) 163 | 164 | const baseStyle = pagination.base 165 | 166 | return ( 167 |
    168 | {/* 169 | * This (label) should probably be an option, and not the default 170 | */} 171 | 172 | Showing {activePage * resultsPerPage - resultsPerPage + 1}- 173 | {Math.min(activePage * resultsPerPage, totalResults)} of {totalResults} 174 | 175 | 176 |
    177 | 208 |
    209 |
    210 | ) 211 | }) 212 | 213 | export default Pagination 214 | -------------------------------------------------------------------------------- /src/Select.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | export interface SelectProps extends React.ComponentPropsWithRef<'select'> { 6 | /** 7 | * Defines the color of the select 8 | */ 9 | valid?: boolean 10 | } 11 | 12 | const Select = React.forwardRef(function Select(props, ref) { 13 | const { valid, children, className, multiple, disabled, ...other } = props 14 | 15 | const { 16 | theme: { select }, 17 | } = useContext(ThemeContext) 18 | 19 | const baseStyle = select.base 20 | const activeStyle = select.active 21 | const validStyle = select.valid 22 | const invalidStyle = select.invalid 23 | const disabledStyle = select.disabled 24 | const selectStyle = select.select 25 | 26 | function hasValidation(valid: boolean | undefined) { 27 | return valid !== undefined 28 | } 29 | 30 | function validationStyle(valid: boolean | undefined): string { 31 | if (hasValidation(valid)) { 32 | return valid ? validStyle : invalidStyle 33 | } 34 | return '' 35 | } 36 | 37 | const cls = classNames( 38 | baseStyle, 39 | // don't apply activeStyle if has valid or disabled 40 | !hasValidation(valid) && !disabled && activeStyle, 41 | // don't apply disabledStyle if has valid 42 | !hasValidation(valid) && disabled && disabledStyle, 43 | validationStyle(valid), 44 | !multiple && selectStyle, 45 | className 46 | ) 47 | 48 | return ( 49 | 52 | ) 53 | }) 54 | 55 | export default Select 56 | -------------------------------------------------------------------------------- /src/Table.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export interface TableProps extends React.TableHTMLAttributes {} 4 | 5 | const Table = React.forwardRef(function Table(props, ref) { 6 | const { children, ...other } = props 7 | return ( 8 |
    9 | 10 | {children} 11 |
    12 |
    13 | ) 14 | }) 15 | 16 | export default Table 17 | -------------------------------------------------------------------------------- /src/TableBody.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | interface Props extends React.HTMLAttributes {} 6 | 7 | const TableBody = React.forwardRef(function TableBody(props, ref) { 8 | const { className, children, ...other } = props 9 | 10 | const { 11 | theme: { tableBody }, 12 | } = useContext(ThemeContext) 13 | 14 | const baseStyle = tableBody.base 15 | 16 | const cls = classNames(baseStyle, className) 17 | 18 | return ( 19 | 20 | {children} 21 | 22 | ) 23 | }) 24 | 25 | export default TableBody 26 | -------------------------------------------------------------------------------- /src/TableCell.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | interface Props extends React.TdHTMLAttributes {} 6 | 7 | const TableCell = React.forwardRef(function TableCell(props, ref) { 8 | const { className, children, ...other } = props 9 | 10 | const { 11 | theme: { tableCell }, 12 | } = useContext(ThemeContext) 13 | 14 | const baseStyle = tableCell.base 15 | 16 | const cls = classNames(baseStyle, className) 17 | 18 | return ( 19 | 20 | {children} 21 | 22 | ) 23 | }) 24 | 25 | export default TableCell 26 | -------------------------------------------------------------------------------- /src/TableContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | interface Props extends React.HTMLAttributes {} 6 | 7 | const TableContainer = React.forwardRef(function TableContainer(props, ref) { 8 | const { className, children, ...other } = props 9 | 10 | const { 11 | theme: { tableContainer }, 12 | } = useContext(ThemeContext) 13 | 14 | const baseStyle = tableContainer.base 15 | 16 | const cls = classNames(baseStyle, className) 17 | 18 | return ( 19 |
    20 | {children} 21 |
    22 | ) 23 | }) 24 | 25 | export default TableContainer 26 | -------------------------------------------------------------------------------- /src/TableFooter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | interface Props extends React.HTMLAttributes {} 6 | 7 | const TableFooter = React.forwardRef(function TableFooter(props, ref) { 8 | const { className, children, ...other } = props 9 | 10 | const { 11 | theme: { tableFooter }, 12 | } = useContext(ThemeContext) 13 | 14 | const baseStyle = tableFooter.base 15 | 16 | const cls = classNames(baseStyle, className) 17 | 18 | return ( 19 |
    20 | {children} 21 |
    22 | ) 23 | }) 24 | 25 | export default TableFooter 26 | -------------------------------------------------------------------------------- /src/TableHeader.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | interface Props extends React.HTMLAttributes {} 6 | 7 | const TableHeader = React.forwardRef(function TableHeader( 8 | props, 9 | ref 10 | ) { 11 | const { className, children, ...other } = props 12 | 13 | const { 14 | theme: { tableHeader }, 15 | } = useContext(ThemeContext) 16 | 17 | const baseStyle = tableHeader.base 18 | 19 | const cls = classNames(baseStyle, className) 20 | 21 | return ( 22 | 23 | {children} 24 | 25 | ) 26 | }) 27 | 28 | export default TableHeader 29 | -------------------------------------------------------------------------------- /src/TableRow.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | interface Props extends React.HTMLAttributes {} 6 | 7 | const TableRow = React.forwardRef(function TableRow(props, ref) { 8 | const { className, children, ...other } = props 9 | 10 | const { 11 | theme: { tableRow }, 12 | } = useContext(ThemeContext) 13 | 14 | const baseStyle = tableRow.base 15 | 16 | const cls = classNames(baseStyle, className) 17 | 18 | return ( 19 | 20 | {children} 21 | 22 | ) 23 | }) 24 | 25 | export default TableRow 26 | -------------------------------------------------------------------------------- /src/Textarea.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { ThemeContext } from './context/ThemeContext' 4 | 5 | export interface TextareaProps extends React.ComponentPropsWithRef<'textarea'> { 6 | /** 7 | * Defines the color of the textarea 8 | */ 9 | valid?: boolean 10 | } 11 | 12 | const Textarea = React.forwardRef(function Textarea( 13 | props, 14 | ref 15 | ) { 16 | const { valid, disabled, className, children, ...other } = props 17 | 18 | const { 19 | theme: { textarea }, 20 | } = useContext(ThemeContext) 21 | 22 | const baseStyle = textarea.base 23 | const activeStyle = textarea.active 24 | const disabledStyle = textarea.disabled 25 | const validStyle = textarea.valid 26 | const invalidStyle = textarea.invalid 27 | 28 | function hasValidation(valid: boolean | undefined) { 29 | return valid !== undefined 30 | } 31 | 32 | function validationStyle(valid: boolean | undefined): string { 33 | if (hasValidation(valid)) { 34 | return valid ? validStyle : invalidStyle 35 | } 36 | return '' 37 | } 38 | 39 | const cls = classNames( 40 | baseStyle, 41 | // don't apply activeStyle if has valid or disabled 42 | !hasValidation(valid) && !disabled && activeStyle, 43 | // don't apply disabledStyle if has valid 44 | !hasValidation(valid) && disabled && disabledStyle, 45 | validationStyle(valid), 46 | className 47 | ) 48 | 49 | return ( 50 | 53 | ) 54 | }) 55 | 56 | export default Textarea 57 | -------------------------------------------------------------------------------- /src/Transition.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * https://gist.github.com/adamwathan/e0a791aa0419098a7ece70028b2e641e 3 | */ 4 | import React, { useContext, useEffect, useRef } from 'react' 5 | import { CSSTransition as ReactCSSTransition } from 'react-transition-group' 6 | 7 | interface TransitionContext { 8 | parent: { 9 | appear?: string 10 | show?: boolean 11 | isInitialRender?: boolean 12 | } 13 | } 14 | const transitionContext = React.createContext({ 15 | parent: {}, 16 | }) 17 | 18 | function useIsInitialRender() { 19 | const isInitialRender = useRef(true) 20 | useEffect(() => { 21 | isInitialRender.current = false 22 | }, []) 23 | return isInitialRender.current 24 | } 25 | 26 | interface TransitionProps { 27 | children?: React.ReactNode 28 | show?: boolean 29 | enter?: string 30 | enterFrom?: string 31 | enterTo?: string 32 | leave?: string 33 | leaveFrom?: string 34 | leaveTo?: string 35 | appear?: string 36 | } 37 | 38 | const CSSTransition: React.FC = function CSSTransition({ 39 | show, 40 | enter = '', 41 | enterFrom = '', 42 | enterTo = '', 43 | leave = '', 44 | leaveFrom = '', 45 | leaveTo = '', 46 | appear, 47 | children, 48 | }) { 49 | const enterClasses = enter.split(' ').filter((s) => s.length) 50 | const enterFromClasses = enterFrom.split(' ').filter((s) => s.length) 51 | const enterToClasses = enterTo.split(' ').filter((s) => s.length) 52 | const leaveClasses = leave.split(' ').filter((s) => s.length) 53 | const leaveFromClasses = leaveFrom.split(' ').filter((s) => s.length) 54 | const leaveToClasses = leaveTo.split(' ').filter((s) => s.length) 55 | 56 | function addClasses(node: HTMLElement, classes: string[]) { 57 | classes.length && node.classList.add(...classes) 58 | } 59 | 60 | function removeClasses(node: HTMLElement, classes: string[]) { 61 | classes.length && node.classList.remove(...classes) 62 | } 63 | 64 | return ( 65 | { 70 | node.addEventListener('transitionend', done, false) 71 | }} 72 | onEnter={(node: HTMLElement) => { 73 | addClasses(node, [...enterClasses, ...enterFromClasses]) 74 | }} 75 | onEntering={(node: HTMLElement) => { 76 | removeClasses(node, enterFromClasses) 77 | addClasses(node, enterToClasses) 78 | }} 79 | onEntered={(node: HTMLElement) => { 80 | removeClasses(node, [...enterToClasses, ...enterClasses]) 81 | }} 82 | onExit={(node: HTMLElement) => { 83 | addClasses(node, [...leaveClasses, ...leaveFromClasses]) 84 | }} 85 | onExiting={(node: HTMLElement) => { 86 | removeClasses(node, leaveFromClasses) 87 | addClasses(node, leaveToClasses) 88 | }} 89 | onExited={(node: HTMLElement) => { 90 | removeClasses(node, [...leaveToClasses, ...leaveClasses]) 91 | }} 92 | > 93 | {children} 94 | 95 | ) 96 | } 97 | 98 | const Transition: React.FC = function Transition({ show, appear, ...rest }) { 99 | const { parent } = useContext(transitionContext) 100 | const isInitialRender = useIsInitialRender() 101 | const isChild = show === undefined 102 | 103 | if (isChild) { 104 | return 105 | } else 106 | return ( 107 | 116 | 117 | 118 | ) 119 | } 120 | 121 | export default Transition 122 | -------------------------------------------------------------------------------- /src/Windmill.tsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useMemo } from 'react' 2 | import { ThemeContext } from './context/ThemeContext' 3 | import defaultTheme from './themes/default' 4 | import { mergeDeep } from './utils/mergeDeep' 5 | import useDarkMode from './utils/useDarkMode' 6 | 7 | interface Props extends React.HTMLAttributes { 8 | children: React.ReactNode 9 | /** 10 | * Defines the styles used throughout the library 11 | */ 12 | theme?: object 13 | /** 14 | * Defines dark mode as the default theme 15 | */ 16 | dark?: boolean 17 | /** 18 | * Allows the change of theme, reading user's preferences and saving them 19 | */ 20 | usePreferences?: boolean 21 | } 22 | 23 | const Windmill: React.FC = ({ 24 | children, 25 | theme: customTheme, 26 | dark, 27 | usePreferences = false, 28 | }) => { 29 | const mergedTheme = mergeDeep(defaultTheme, customTheme) 30 | const [mode, setMode, toggleMode] = useDarkMode(usePreferences) 31 | 32 | useLayoutEffect(() => { 33 | if (dark) { 34 | if (setMode != null) { 35 | setMode('dark') 36 | } 37 | document.documentElement.classList.add(`dark`) 38 | } 39 | }, [dark]) 40 | 41 | const value = useMemo( 42 | () => ({ 43 | theme: mergedTheme, 44 | mode, 45 | toggleMode, 46 | }), 47 | [mode] 48 | ) 49 | 50 | return {children} 51 | } 52 | 53 | export default Windmill 54 | -------------------------------------------------------------------------------- /src/__tests__/Alert.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mount } from 'enzyme' 3 | import Alert, { SuccessIcon, DangerIcon, WarningIcon, InfoIcon, NeutralIcon } from '../Alert' 4 | 5 | describe('Avatar', () => { 6 | it('should render without crashing', () => { 7 | mount() 8 | }) 9 | 10 | it('should not contain a close button', () => { 11 | const wrapper = mount() 12 | 13 | expect(wrapper.find('button')).toHaveLength(0) 14 | }) 15 | 16 | it('should contain a close button', () => { 17 | const wrapper = mount( {}} />) 18 | 19 | expect(wrapper.find('button')).toHaveLength(1) 20 | }) 21 | 22 | it('should call a function when close button is clicked', () => { 23 | const onClose = jest.fn() 24 | const wrapper = mount() 25 | 26 | wrapper.find('button[aria-label="close"]').simulate('click') 27 | 28 | expect(onClose).toHaveBeenCalled() 29 | }) 30 | 31 | it('should render a success icon', () => { 32 | const wrapper = mount() 33 | const Icon = SuccessIcon 34 | 35 | expect(wrapper.find(Icon)).toHaveLength(1) 36 | }) 37 | 38 | it('should render a danger icon', () => { 39 | const wrapper = mount() 40 | const Icon = DangerIcon 41 | 42 | expect(wrapper.find(Icon)).toHaveLength(1) 43 | }) 44 | 45 | it('should render an info icon', () => { 46 | const wrapper = mount() 47 | const Icon = InfoIcon 48 | 49 | expect(wrapper.find(Icon)).toHaveLength(1) 50 | }) 51 | 52 | it('should render a warning icon', () => { 53 | const wrapper = mount() 54 | const Icon = WarningIcon 55 | 56 | expect(wrapper.find(Icon)).toHaveLength(1) 57 | }) 58 | 59 | it('should render a neutral icon', () => { 60 | const wrapper = mount() 61 | const Icon = NeutralIcon 62 | 63 | expect(wrapper.find(Icon)).toHaveLength(1) 64 | }) 65 | 66 | it('should render a neutral icon for an invalid type', () => { 67 | // @ts-expect-error 68 | const wrapper = mount() 69 | const Icon = NeutralIcon 70 | 71 | expect(wrapper.find(Icon)).toHaveLength(1) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /src/__tests__/Avatar.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mount } from 'enzyme' 3 | import Avatar from '../Avatar' 4 | 5 | describe('Avatar', () => { 6 | it('should render without crashing', () => { 7 | mount() 8 | }) 9 | 10 | it('should render with base styles', () => { 11 | const expected = 'relative rounded-full inline-block' 12 | const wrapper = mount() 13 | 14 | expect(wrapper.find(Avatar).getDOMNode().getAttribute('class')).toContain(expected) 15 | }) 16 | 17 | it('should render with large styles', () => { 18 | const expected = 'w-10 h-10' 19 | const wrapper = mount() 20 | 21 | expect(wrapper.find(Avatar).getDOMNode().getAttribute('class')).toContain(expected) 22 | }) 23 | 24 | it('should render with regular styles using prop', () => { 25 | const expected = 'w-8 h-8' 26 | const wrapper = mount() 27 | 28 | expect(wrapper.find(Avatar).getDOMNode().getAttribute('class')).toContain(expected) 29 | }) 30 | 31 | it('should render with regular styles by default', () => { 32 | const expected = 'w-8 h-8' 33 | const wrapper = mount() 34 | 35 | expect(wrapper.find(Avatar).getDOMNode().getAttribute('class')).toContain(expected) 36 | }) 37 | 38 | it('should render with small styles', () => { 39 | const expected = 'w-6 h-6' 40 | const wrapper = mount() 41 | 42 | expect(wrapper.find(Avatar).getDOMNode().getAttribute('class')).toContain(expected) 43 | }) 44 | 45 | it('should contain an image with alt text', () => { 46 | const expected = 'Lorem' 47 | const wrapper = mount() 48 | 49 | expect(wrapper.find('img').getDOMNode().getAttribute('alt')).toContain(expected) 50 | }) 51 | 52 | it('should contain an image with the correct src', () => { 53 | const expected = 'test' 54 | const wrapper = mount() 55 | 56 | expect(wrapper.find('img').getDOMNode().getAttribute('src')).toContain(expected) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/__tests__/Backdrop.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mount } from 'enzyme' 3 | import Backdrop from '../Backdrop' 4 | 5 | describe('Backdrop', () => { 6 | it('should render without crashing', () => { 7 | mount() 8 | }) 9 | 10 | it('should render with base styles', () => { 11 | const expected = 12 | 'fixed inset-0 z-40 flex items-end bg-black bg-opacity-50 sm:items-center sm:justify-center' 13 | const wrapper = mount() 14 | 15 | expect(wrapper.find(Backdrop).getDOMNode().getAttribute('class')).toContain(expected) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/__tests__/Badge.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mount } from 'enzyme' 3 | import Badge from '../Badge' 4 | 5 | describe('Badge', () => { 6 | it('should render without crashing', () => { 7 | mount() 8 | }) 9 | 10 | it('should render with base styles', () => { 11 | const expected = 'inline-flex px-2 text-xs font-medium leading-5 rounded-full' 12 | const wrapper = mount() 13 | 14 | expect(wrapper.find('span').getDOMNode().getAttribute('class')).toContain(expected) 15 | }) 16 | 17 | it('should render with success styles', () => { 18 | const expected = 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' 19 | const wrapper = mount() 20 | 21 | expect(wrapper.find('span').getDOMNode().getAttribute('class')).toContain(expected) 22 | }) 23 | 24 | it('should render with danger styles', () => { 25 | const expected = 'text-red-700 bg-red-100 dark:text-red-100 dark:bg-red-700' 26 | const wrapper = mount() 27 | 28 | expect(wrapper.find('span').getDOMNode().getAttribute('class')).toContain(expected) 29 | }) 30 | 31 | it('should render with warning styles', () => { 32 | const expected = 'text-orange-700 bg-orange-100 dark:text-white dark:bg-orange-600' 33 | const wrapper = mount() 34 | 35 | expect(wrapper.find('span').getDOMNode().getAttribute('class')).toContain(expected) 36 | }) 37 | 38 | it('should render with neutral styles', () => { 39 | const expected = 'text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700' 40 | const wrapper = mount() 41 | 42 | expect(wrapper.find('span').getDOMNode().getAttribute('class')).toContain(expected) 43 | }) 44 | 45 | it('should render with primary styles', () => { 46 | const expected = 'text-purple-700 bg-purple-100 dark:text-white dark:bg-purple-600' 47 | const wrapper = mount() 48 | 49 | expect(wrapper.find('span').getDOMNode().getAttribute('class')).toContain(expected) 50 | }) 51 | 52 | it('should render with primary styles when no type is used', () => { 53 | const expected = 'text-purple-700 bg-purple-100 dark:text-white dark:bg-purple-600' 54 | const wrapper = mount() 55 | 56 | expect(wrapper.find('span').getDOMNode().getAttribute('class')).toContain(expected) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/__tests__/Button.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mount } from 'enzyme' 3 | import Button from '../Button' 4 | import HeartIcon from './utils/heart.svg' 5 | import theme from '../themes/default' 6 | 7 | describe('Base Button', () => { 8 | it('should render without crashing', () => { 9 | mount() 56 | 57 | expect(wrapper.find('button').text()).toBe('Hi') 58 | }) 59 | 60 | it('should contain base classes', () => { 61 | const expected = 62 | 'inline-flex items-center justify-center cursor-pointer leading-5 transition-colors duration-150 font-medium focus:outline-none' 63 | const wrapper = mount( 240 | ) 241 | 242 | expect(wrapper.find('svg')).toBeDefined() 243 | }) 244 | 245 | it('should contain an svg passed as prop', () => { 246 | const wrapper = mount() 247 | 248 | expect(wrapper.find('svg')).toBeDefined() 249 | }) 250 | 251 | it('should render an icon as the first child of the button, using icon', () => { 252 | const wrapper = mount() 253 | 254 | expect(wrapper.find('button').children()).toHaveLength(2) 255 | expect(wrapper.find('button').childAt(0).type()).toBe(HeartIcon) 256 | expect(wrapper.find('button').childAt(1).text()).toBe('Lorem') 257 | }) 258 | 259 | it('should render an icon as the first child of the button, using iconLeft', () => { 260 | const wrapper = mount() 261 | 262 | expect(wrapper.find('button').children()).toHaveLength(2) 263 | expect(wrapper.find('button').childAt(0).type()).toBe(HeartIcon) 264 | expect(wrapper.find('button').childAt(1).text()).toBe('Lorem') 265 | }) 266 | 267 | it('should render an icon as the last child of the button', () => { 268 | const wrapper = mount() 269 | 270 | expect(wrapper.find('button').children()).toHaveLength(2) 271 | expect(wrapper.find('button').childAt(0).text()).toBe('Lorem') 272 | expect(wrapper.find('button').childAt(1).type()).toBe(HeartIcon) 273 | }) 274 | 275 | it('should not contain left or right styles', () => { 276 | const expected = 'mr-2 -ml-1 ml-2 -mr-1' 277 | const wrapper = mount() 285 | 286 | expect(wrapper.find('svg').getDOMNode().getAttribute('class')).toContain(expected) 287 | }) 288 | 289 | it('should render an icon with right styles', () => { 290 | const expected = 'ml-2 -mr-1' 291 | const wrapper = mount() 292 | 293 | expect(wrapper.find('svg').getDOMNode().getAttribute('class')).toContain(expected) 294 | }) 295 | 296 | it('should render a button with regular styles if children is present', () => { 297 | const expected = 'px-4 py-2 rounded-lg text-sm' 298 | const wrapper = mount() 299 | 300 | expect(wrapper.find('button').getDOMNode().getAttribute('class')).toContain(expected) 301 | }) 302 | 303 | it('should contain regular sized button icon classes', () => { 304 | const expectedButton = 'p-2 rounded-lg' 305 | const expectedSvg = 'h-5 w-5' 306 | const wrapper = mount(