├── .npmignore
├── .prettierignore
├── CHANGELOG.md
├── .prettierrc
├── .babelrc
├── bsconfig.json
├── .editorconfig
├── src
├── components
│ ├── DangerousHTML.re
│ ├── Loader.re
│ ├── Radio.js
│ ├── CodeBlock.re
│ ├── RenderIf.re
│ ├── Box.re
│ ├── Portal.js
│ ├── Tab.js
│ ├── ProgressBar.re
│ ├── Margin.re
│ ├── Padding.re
│ ├── Breadcrumbs.js
│ ├── Text.re
│ ├── Flex.re
│ ├── Card.re
│ ├── Panel.re
│ ├── Checkbox.re
│ ├── Navbar.re
│ ├── TruncateText.js
│ ├── Button.re
│ ├── Tabs.js
│ ├── Table.js
│ ├── Modal.re
│ ├── Article.re
│ ├── TextField.re
│ ├── ImageCarousel.js
│ ├── Select.re
│ ├── TextArea.re
│ ├── Collapsible.re
│ ├── Dropdown.re
│ ├── MegaFooter.js
│ └── Alert.re
├── styles
│ └── main.css
└── index.js
├── .gitignore
├── LICENSE
├── rollup.config.js
├── README.md
└── package.json
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .babelrc
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | lib/
3 | dist/
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.5.5
2 | - Fix broken components
3 | - Fix optional props
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "singleQuote": true,
4 | "semi": false
5 | }
6 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "modules": false
7 | }
8 | ],
9 | "@babel/stage-0",
10 | "@babel/react"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/bsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tails-ui",
3 | "sources": [
4 | {
5 | "dir": "src",
6 | "subdirs": true
7 | }
8 | ],
9 | "bs-dependencies": ["reason-react"],
10 | "reason": {
11 | "react-jsx": 2
12 | },
13 | "package-specs": ["es6"],
14 | "bsc-flags": ["-bs-super-errors"],
15 | "refmt": 3
16 | }
17 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*]
7 | charset = utf-8
8 | indent_style = space
9 | indent_size = 2
10 | end_of_line = lf
11 | insert_final_newline = true
12 | trim_trailing_whitespace = true
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/src/components/DangerousHTML.re:
--------------------------------------------------------------------------------
1 | let dangerousHtml: string => Js.t('a) = html => {"__html": html};
2 |
3 | let component = ReasonReact.statelessComponent("DangerousHTML");
4 |
5 | let make = (~children) => {
6 | ...component,
7 | render: _self =>
8 | };
9 |
10 | let default =
11 | ReasonReact.wrapReasonForJs(~component, jsProps =>
12 | make(~children=jsProps##children)
13 | );
14 |
--------------------------------------------------------------------------------
/src/components/Loader.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("Loader");
2 |
3 | let make = (~color="teal", _children) => {
4 | ...component,
5 | render: _self =>
6 |
9 | };
10 |
11 | let default =
12 | ReasonReact.wrapReasonForJs(~component, jsProps =>
13 | make(~color=jsProps##color, [||])
14 | );
15 |
--------------------------------------------------------------------------------
/src/components/Radio.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Radio = ({ checked, disabled, handleChange, label, ...props }) => (
4 |
5 |
12 | {label && {label} }
13 |
14 | )
15 |
16 | export default Radio
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 | /dist
12 |
13 | # bucklescript
14 | /lib
15 | /types
16 | .merlin
17 | .bsb.lock
18 |
19 | # misc
20 | .DS_Store
21 | .env.local
22 | .env.development.local
23 | .env.test.local
24 | .env.production.local
25 |
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 |
--------------------------------------------------------------------------------
/src/components/CodeBlock.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("CodeBlock");
2 |
3 | let make = (~children) => {
4 | ...component,
5 | render: _self =>
6 |
7 |
8 | (ReasonReact.arrayToElement(children))
9 |
10 |
11 | };
12 |
13 | let default =
14 | ReasonReact.wrapReasonForJs(~component, jsProps =>
15 | make(~children=jsProps##children)
16 | );
17 |
--------------------------------------------------------------------------------
/src/components/RenderIf.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("RenderIf");
2 |
3 | let make = (~condition, ~children) => {
4 | ...component,
5 | render: _self =>
6 | Js.Boolean.to_js_boolean(condition) === Js.Boolean.to_js_boolean(true) ?
7 | ReasonReact.arrayToElement(children) : ReasonReact.nullElement
8 | };
9 |
10 | let default =
11 | ReasonReact.wrapReasonForJs(~component, jsProps =>
12 | make(~condition=jsProps##condition, ~children=jsProps##children)
13 | );
14 |
--------------------------------------------------------------------------------
/src/components/Box.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("Box");
2 |
3 | let make = (~sm="full", ~md="full", ~lg="full", ~xl="full", ~children) => {
4 | ...component,
5 | render: (_) =>
6 |
7 | (ReasonReact.arrayToElement(children))
8 |
9 | };
10 |
11 | let default =
12 | ReasonReact.wrapReasonForJs(~component, jsProps =>
13 | make(
14 | ~sm=jsProps##sm,
15 | ~md=jsProps##md,
16 | ~lg=jsProps##lg,
17 | ~xl=jsProps##xl,
18 | ~children=jsProps##children
19 | )
20 | );
21 |
--------------------------------------------------------------------------------
/src/styles/main.css:
--------------------------------------------------------------------------------
1 | button:focus {
2 | outline: none;
3 | }
4 |
5 | textarea:focus {
6 | outline: none;
7 | }
8 |
9 | input:focus {
10 | outline: none;
11 | }
12 |
13 | select:focus {
14 | outline: none;
15 | }
16 |
17 | table {
18 | border-collapse: collapse;
19 | border-spacing: 0;
20 | }
21 |
22 | tr:nth-child(even) {
23 | background-color: #f1f5f8;
24 | }
25 |
26 | @keyframes spin {
27 | from {
28 | transform: rotate(0deg);
29 | }
30 | to {
31 | transform: rotate(360deg);
32 | }
33 | }
34 |
35 | .loader {
36 | animation: spin 1s infinite linear;
37 | }
38 |
39 | .top-pin {
40 | top: 110%;
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/Portal.js:
--------------------------------------------------------------------------------
1 | import { PureComponent } from 'react'
2 | import { createPortal } from 'react-dom'
3 |
4 | export default class Portal extends PureComponent {
5 | componentWillUnmount() {
6 | if (this.defaultNode) {
7 | document.body.removeChild(this.defaultNode)
8 | }
9 | this.defaultNode = null
10 | }
11 |
12 | render() {
13 | const { domNode } = this.props
14 |
15 | if (!domNode && !this.defaultNode) {
16 | this.defaultNode = document.createElement('div')
17 | document.body.appendChild(this.defaultNode)
18 | }
19 |
20 | return createPortal(
21 | this.props.children,
22 | this.props.domNode || this.defaultNode
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Tab.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Tab = ({ onClick, tabIndex, title, isActive, color }) => (
4 |
5 | {
7 | event.preventDefault()
8 | onClick(tabIndex)
9 | }}
10 | className={`inline-block no-underline ${
11 | isActive ? 'border-blue' : 'border-white'
12 | } ${!isActive &&
13 | 'hover:border-grey hover:bg-grey-lighter'} rounded py-1 px-3 ${isActive &&
14 | `bg-${color}`} ${!isActive && `text-${color}`} text-white`}
15 | href="#"
16 | >
17 | {title}
18 |
19 |
20 | )
21 |
22 | Tab.defaultProps = {
23 | color: 'teal'
24 | }
25 |
26 | export default Tab
27 |
--------------------------------------------------------------------------------
/src/components/ProgressBar.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("ProgressBar");
2 |
3 | let make = (~color="teal", ~percent="50%", _children) => {
4 | ...component,
5 | render: _self =>
6 |
7 |
8 |
11 | (ReasonReact.stringToElement(percent))
12 |
13 |
14 |
15 | };
16 |
17 | let default =
18 | ReasonReact.wrapReasonForJs(~component, jsProps =>
19 | make(~color=jsProps##color, ~percent=jsProps##percent, [||])
20 | );
21 |
--------------------------------------------------------------------------------
/src/components/Margin.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("Margin");
2 |
3 | let make =
4 | (
5 | ~margin=?,
6 | ~marginLeft=?,
7 | ~marginRight=?,
8 | ~marginBottom=?,
9 | ~marginTop=?,
10 | _children
11 | ) => {
12 | ...component,
13 | render: _self =>
14 |
17 | };
18 |
19 | let default =
20 | ReasonReact.wrapReasonForJs(~component, jsProps =>
21 | make(
22 | ~margin=?jsProps##margin,
23 | ~marginLeft=?jsProps##marginLeft,
24 | ~marginRight=?jsProps##marginRight,
25 | ~marginBottom=?jsProps##marginBottom,
26 | ~marginTop=?jsProps##marginTop,
27 | [||]
28 | )
29 | );
30 |
--------------------------------------------------------------------------------
/src/components/Padding.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("Padding");
2 |
3 | let make =
4 | (
5 | ~padding=?,
6 | ~paddingLeft=?,
7 | ~paddingRight=?,
8 | ~paddingBottom=?,
9 | ~paddingTop=?,
10 | _children
11 | ) => {
12 | ...component,
13 | render: _self =>
14 |
17 | };
18 |
19 | let default =
20 | ReasonReact.wrapReasonForJs(~component, jsProps =>
21 | make(
22 | ~padding=?jsProps##padding,
23 | ~paddingLeft=?jsProps##paddingLeft,
24 | ~paddingRight=?jsProps##paddingRight,
25 | ~paddingBottom=?jsProps##paddingBottom,
26 | ~paddingTop=?jsProps##paddingTop,
27 | [||]
28 | )
29 | );
30 |
--------------------------------------------------------------------------------
/src/components/Breadcrumbs.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Breadcrumbs = ({ bgColor, crumbs, textColor }) => (
4 |
5 |
6 | {crumbs.map(crumb => (
7 |
8 |
9 | {React.cloneElement(crumb.value, {
10 | className: `${textColor} font-bold`
11 | })}
12 |
13 | {crumb.hasSeparator && (
14 |
15 | /
16 |
17 | )}
18 |
19 | ))}
20 |
21 |
22 | )
23 |
24 | Breadcrumbs.defaultProps = {
25 | bgColor: 'bg-grey-light',
26 | textColor: 'text-blue'
27 | }
28 |
29 | export default Breadcrumbs
30 |
--------------------------------------------------------------------------------
/src/components/Text.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("Text");
2 |
3 | let make =
4 | (
5 | ~font="sans",
6 | ~color="grey-darker",
7 | ~size="sm",
8 | ~weight="normal",
9 | ~align="left",
10 | ~fontStyle="normal-case",
11 | ~children
12 | ) => {
13 | ...component,
14 | render: _self =>
15 |
17 | (ReasonReact.arrayToElement(children))
18 |
19 | };
20 |
21 | let default =
22 | ReasonReact.wrapReasonForJs(~component, jsProps =>
23 | make(
24 | ~font=jsProps##font,
25 | ~color=jsProps##color,
26 | ~size=jsProps##size,
27 | ~weight=jsProps##weight,
28 | ~align=jsProps##align,
29 | ~fontStyle=jsProps##fontStyle,
30 | ~children=jsProps##children
31 | )
32 | );
33 |
--------------------------------------------------------------------------------
/src/components/Flex.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("Flex");
2 |
3 | let make =
4 | (
5 | ~display="flex",
6 | ~direction="row",
7 | ~wrap="no-wrap",
8 | ~justify="start",
9 | ~align="start",
10 | ~height="full",
11 | ~width="full",
12 | ~children
13 | ) => {
14 | ...component,
15 | render: _self =>
16 |
18 | (ReasonReact.arrayToElement(children))
19 |
20 | };
21 |
22 | let default =
23 | ReasonReact.wrapReasonForJs(~component, jsProps =>
24 | make(
25 | ~display=jsProps##display,
26 | ~direction=jsProps##direction,
27 | ~wrap=jsProps##wrap,
28 | ~justify=jsProps##justify,
29 | ~align=jsProps##align,
30 | ~height=jsProps##height,
31 | ~width=jsProps##width,
32 | ~children=jsProps##children
33 | )
34 | );
35 |
--------------------------------------------------------------------------------
/src/components/Card.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("Card");
2 |
3 | let make =
4 | (
5 | ~title="Card Title",
6 | ~footer: array(ReasonReact.reactElement)=[||],
7 | ~children
8 | ) => {
9 | ...component,
10 | render: _self =>
11 |
12 |
13 |
14 | (ReasonReact.stringToElement(title))
15 |
16 | (ReasonReact.arrayToElement(children))
17 |
18 |
(ReasonReact.arrayToElement(footer))
19 |
20 | };
21 |
22 | let default =
23 | ReasonReact.wrapReasonForJs(~component, jsProps =>
24 | make(
25 | ~title=jsProps##title,
26 | ~footer=
27 | switch (Js.Null_undefined.to_opt(jsProps##footer)) {
28 | | None => [||]
29 | | Some(footer) => footer
30 | },
31 | ~children=jsProps##children
32 | )
33 | );
34 |
--------------------------------------------------------------------------------
/src/components/Panel.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("Panel");
2 |
3 | let make = (~color="grey", ~title="Panel Title", ~extra=?, ~children) => {
4 | ...component,
5 | render: _self => {
6 | let extraComponent =
7 | switch extra {
8 | | None => ReasonReact.nullElement
9 | | Some(extra) => ReasonReact.arrayToElement(extra)
10 | };
11 |
12 |
14 |
15 | (ReasonReact.stringToElement(title))
16 |
17 |
extraComponent
18 |
19 |
(ReasonReact.arrayToElement(children))
20 |
;
21 | }
22 | };
23 |
24 | let default =
25 | ReasonReact.wrapReasonForJs(~component, jsProps =>
26 | make(
27 | ~color=jsProps##color,
28 | ~title=jsProps##title,
29 | ~extra=?jsProps##extra,
30 | ~children=jsProps##children
31 | )
32 | );
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Tyler Knipfer
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 |
--------------------------------------------------------------------------------
/src/components/Checkbox.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("Checkbox");
2 |
3 | let make = (~checked=false, ~disabled=false, ~label=?, _children) => {
4 | ...component,
5 | render: _self => {
6 | let labelComponent =
7 | switch label {
8 | | None => ReasonReact.nullElement
9 | | Some(label) => ReasonReact.stringToElement(label)
10 | };
11 |
12 |
17 | (
18 | labelComponent === ReasonReact.stringToElement("") ?
19 |
21 | labelComponent
22 | :
23 | ReasonReact.nullElement
24 | )
25 |
;
26 | }
27 | };
28 |
29 | let default =
30 | ReasonReact.wrapReasonForJs(~component, jsProps =>
31 | make(
32 | ~checked=jsProps##checked,
33 | ~disabled=jsProps##disabled,
34 | ~label=jsProps##label,
35 | [||]
36 | )
37 | );
38 |
--------------------------------------------------------------------------------
/src/components/Navbar.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("Navbar");
2 |
3 | let make = (~color="teal", ~brand="Navbar", ~navRight=?, ~children) => {
4 | ...component,
5 | render: _self => {
6 | let navRightComponent =
7 | switch navRight {
8 | | None => ReasonReact.nullElement
9 | | Some(navRight) => ReasonReact.arrayToElement(navRight)
10 | };
11 |
12 |
13 |
14 | (ReasonReact.stringToElement(brand))
15 |
16 |
navRightComponent
17 |
18 |
20 | (ReasonReact.arrayToElement(children))
21 |
22 |
;
23 | }
24 | };
25 |
26 | let default =
27 | ReasonReact.wrapReasonForJs(~component, jsProps =>
28 | make(
29 | ~color=jsProps##color,
30 | ~brand=jsProps##brand,
31 | ~navRight=?jsProps##navRight,
32 | ~children=jsProps##children
33 | )
34 | );
35 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel'
2 | import bucklescript from 'rollup-plugin-bucklescript'
3 | import commonjs from 'rollup-plugin-commonjs'
4 | import filesize from 'rollup-plugin-filesize'
5 | import { minify } from 'uglify-es'
6 | import pkg from './package.json'
7 | import postcss from 'rollup-plugin-postcss'
8 | import replace from 'rollup-plugin-replace'
9 | import resolve from 'rollup-plugin-node-resolve'
10 | import uglify from 'rollup-plugin-uglify'
11 |
12 | export default {
13 | input: 'src/index.js',
14 | output: [
15 | {
16 | file: pkg.main,
17 | format: 'cjs',
18 | sourcemap: true
19 | }
20 | ],
21 | external: ['react', 'react-dom', 'reason-react'],
22 | plugins: [
23 | postcss({
24 | extract: true
25 | }),
26 | bucklescript(),
27 | babel({
28 | exclude: 'node_modules/**'
29 | }),
30 | resolve(),
31 | commonjs(),
32 | replace({
33 | exclude: 'node_modules/**',
34 | 'process.env.NODE_ENV': JSON.stringify(
35 | process.env.NODE_ENV || 'development'
36 | )
37 | }),
38 | process.env.NODE_ENV === 'production' && filesize(),
39 | process.env.NODE_ENV === 'production' && uglify({}, minify)
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/TruncateText.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 |
3 | export default class TruncateText extends PureComponent {
4 | static defaultProps = {
5 | wordCount: 10,
6 | toggleColor: 'teal'
7 | }
8 |
9 | state = {
10 | showMore: false
11 | }
12 |
13 | toggleShowMore = () => this.setState({ showMore: !this.state.showMore })
14 |
15 | render() {
16 | const { showMore } = this.state
17 | const { children, wordCount, toggleColor } = this.props
18 |
19 | const spliced = children
20 | .split(' ')
21 | .splice(0, wordCount)
22 | .join(' ')
23 |
24 | const truncated =
25 | spliced.split('').lastIndexOf(',') === spliced.length - 1
26 | ? spliced.slice(0, spliced.length - 1)
27 | : spliced
28 |
29 | return (
30 |
31 | {showMore ? children : truncated}
32 | {children.split(' ').length > wordCount && (
33 | this.toggleShowMore()}
36 | >
37 | {!showMore ? ...more : ...less }
38 |
39 | )}
40 |
41 | )
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/Button.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("Button");
2 |
3 | let make =
4 | (
5 | ~htmlType="button",
6 | ~fontColor="white",
7 | ~loading=false,
8 | ~width="full",
9 | ~backgroundColor="teal",
10 | ~borderColor="teal",
11 | ~borderRadius="rounded",
12 | ~children
13 | ) => {
14 | ...component,
15 | render: _self =>
16 |
19 | (
20 | loading === true ?
21 | ReasonReact.stringToElement("loading...") :
22 | ReasonReact.arrayToElement(children)
23 | )
24 |
25 | };
26 |
27 | let default =
28 | ReasonReact.wrapReasonForJs(~component, jsProps =>
29 | make(
30 | ~htmlType=jsProps##htmlType,
31 | ~fontColor=jsProps##fontColor,
32 | ~loading=jsProps##loading,
33 | ~width=jsProps##width,
34 | ~backgroundColor=jsProps##backgroundColor,
35 | ~borderColor=jsProps##borderColor,
36 | ~borderRadius=jsProps##borderRadius,
37 | ~children=jsProps##children
38 | )
39 | );
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tails-UI
2 | React UI components using **tailwindcss**
3 |
4 | ## Components:
5 | - Alert
6 | - Article
7 | - Box
8 | - Breadcrumbs
9 | - Button
10 | - Card
11 | - CodeBlock
12 | - DangerousHTML
13 | - Dropdown
14 | - Flex
15 | - ImageCarousel
16 | - Loader
17 | - MegaFooter
18 | - Modal
19 | - Navbar
20 | - Panel
21 | - Portal
22 | - ProgressBar
23 | - RenderIf
24 | - Select
25 | - Space
26 | - Tab
27 | - Table
28 | - Tabs
29 | - Text
30 | - TextArea
31 | - TextField
32 | - TruncateText
33 |
34 | ## Installation:
35 | `npm i tails-ui`
36 |
37 | ## Usage:
38 | - Make sure to import the tails-ui css file `import 'tails-ui/dist/index.css'`
39 | - Documentation site is coming soon with all the component api's
40 | ``` js
41 | import React from 'react'
42 | import { Button } from 'tails-ui'
43 |
44 | const Button = () => (
45 |
51 | Submit
52 |
53 | )
54 |
55 | export default Button
56 |
57 | ```
58 |
59 | ## Running Locally:
60 | - You can run `npm link` inside **tails-ui** and then `npm link tails-ui` inside a project you have created to test the components locally.
61 | - You can run `npm start` to start **tails-ui** in watch mode and it will compile any new components you add.
62 | - `npm test` will run jest
63 |
64 |
--------------------------------------------------------------------------------
/src/components/Tabs.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 |
3 | export default class Tabs extends PureComponent {
4 | static defaultProps = {
5 | position: 'start'
6 | }
7 |
8 | state = {
9 | activeTabIndex: this.props.defaultActiveTabIndex
10 | }
11 |
12 | handleTabClick = tabIndex => {
13 | this.setState({
14 | activeTabIndex: tabIndex
15 | })
16 | }
17 |
18 | renderChildrenWithTabsApiAsProps = () => {
19 | return React.Children.map(this.props.children, (child, index) => {
20 | return React.cloneElement(child, {
21 | onClick: this.handleTabClick,
22 | tabIndex: index,
23 | isActive: index === this.state.activeTabIndex
24 | })
25 | })
26 | }
27 |
28 | renderActiveTabContent = () => {
29 | const { children } = this.props
30 | const { activeTabIndex } = this.state
31 | if (children[activeTabIndex]) {
32 | return children[activeTabIndex].props.children
33 | }
34 | }
35 |
36 | render() {
37 | const { position } = this.props
38 | return (
39 |
40 |
41 | {this.renderChildrenWithTabsApiAsProps()}
42 |
43 | {this.renderActiveTabContent()}
44 |
45 | )
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/Table.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 |
3 | export default class Table extends PureComponent {
4 | generateHeaders = () => {
5 | const cols = this.props.cols
6 | return cols.map(colData => {
7 | return (
8 |
9 | {colData.label}
10 |
11 | )
12 | })
13 | }
14 |
15 | generateRows = () => {
16 | const cols = this.props.cols
17 | const data = this.props.data
18 | return data.map(item => {
19 | var cells = cols.map((colData, index) => {
20 | return (
21 |
22 | {item[colData.key]}
23 |
24 | )
25 | })
26 | return (
27 |
28 | {cells}
29 |
30 | )
31 | })
32 | }
33 |
34 | render() {
35 | const headers = this.generateHeaders()
36 | const rows = this.generateRows()
37 | return (
38 |
39 |
40 |
41 | {headers}
42 |
43 | {rows}
44 |
45 |
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/Modal.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("Modal");
2 |
3 | let make = (~title="Modal Title", ~footer=?, ~children) => {
4 | ...component,
5 | render: _self => {
6 | let footerComponent =
7 | switch footer {
8 | | None => ReasonReact.nullElement
9 | | Some(footer) => ReasonReact.arrayToElement(footer)
10 | };
11 |
12 |
13 |
15 |
16 |
17 | (ReasonReact.stringToElement(title))
18 |
19 |
20 |
21 | (ReasonReact.stringToElement("close"))
22 |
23 |
24 |
25 |
(ReasonReact.arrayToElement(children))
26 |
footerComponent
27 |
28 |
;
29 | }
30 | };
31 |
32 | let default =
33 | ReasonReact.wrapReasonForJs(~component, jsProps =>
34 | make(
35 | ~title=jsProps##title,
36 | ~footer=?jsProps##footer,
37 | ~children=jsProps##children
38 | )
39 | );
40 |
--------------------------------------------------------------------------------
/src/components/Article.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("Article");
2 |
3 | let make =
4 | (
5 | ~title="Article Title",
6 | ~author="Article Author",
7 | ~description="Article Description",
8 | ~abstract="Article Abstract",
9 | ~footer: array(ReasonReact.reactElement)=[||],
10 | _children
11 | ) => {
12 | ...component,
13 | render: _self =>
14 |
15 |
16 | (ReasonReact.stringToElement(title))
17 |
18 |
(ReasonReact.stringToElement(author))
19 |
20 | (ReasonReact.stringToElement(description))
21 |
22 |
23 | (ReasonReact.stringToElement(abstract))
24 |
25 |
26 | (ReasonReact.arrayToElement(footer))
27 |
28 |
29 | };
30 |
31 | let default =
32 | ReasonReact.wrapReasonForJs(~component, jsProps =>
33 | make(
34 | ~title=jsProps##title,
35 | ~author=jsProps##author,
36 | ~description=jsProps##description,
37 | ~abstract=jsProps##abstract,
38 | ~footer=
39 | switch (Js.Null_undefined.to_opt(jsProps##footer)) {
40 | | None => [||]
41 | | Some(footer) => footer
42 | },
43 | [||]
44 | )
45 | );
46 |
--------------------------------------------------------------------------------
/src/components/TextField.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("TextField");
2 |
3 | let make =
4 | (
5 | ~label=?,
6 | ~value=?,
7 | ~htmlType="text",
8 | ~name=?,
9 | ~placeholder=?,
10 | ~color="teal",
11 | ~hasError=false,
12 | _children
13 | ) => {
14 | ...component,
15 | render: _self => {
16 | let errorStyle =
17 | [
18 | Js.Boolean.to_js_boolean(hasError) === Js.Boolean.to_js_boolean(true) ?
19 | "border-red hover:border-red" :
20 | {j|border-grey-light hover:border-$(color)|j}
21 | ]
22 | |> String.concat("");
23 | let labelComponent =
24 | switch label {
25 | | None => ReasonReact.nullElement
26 | | Some(label) => ReasonReact.stringToElement(label)
27 | };
28 |
29 | (
30 | labelComponent === ReasonReact.stringToElement("") ?
31 |
33 | labelComponent
34 | :
35 | ReasonReact.nullElement
36 | )
37 |
44 |
;
45 | }
46 | };
47 |
48 | let default =
49 | ReasonReact.wrapReasonForJs(~component, jsProps =>
50 | make(
51 | ~label=?jsProps##label,
52 | ~value=?jsProps##value,
53 | ~htmlType=jsProps##htmlType,
54 | ~name=?jsProps##name,
55 | ~placeholder=?jsProps##placeholder,
56 | ~color=jsProps##color,
57 | ~hasError=?jsProps##hasError,
58 | [||]
59 | )
60 | );
61 |
--------------------------------------------------------------------------------
/src/components/ImageCarousel.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 |
3 | export default class ImageCarousel extends PureComponent {
4 | static defaultProps = {
5 | color: 'grey'
6 | }
7 |
8 | state = {
9 | selectedImage: 0
10 | }
11 |
12 | handleSelect = selectedImage => {
13 | this.setState({ selectedImage })
14 | }
15 |
16 | render() {
17 | const { images, color } = this.props
18 | const { selectedImage } = this.state
19 | const showNav = images && images.length > 1
20 | return (
21 |
22 |
25 | {images &&
26 | images[selectedImage].src && (
27 |
32 | )}
33 |
34 | {showNav && (
35 |
36 | {images &&
37 | images.map((image, i) => (
38 |
42 |
this.handleSelect(i)}
45 | >
46 |
51 |
52 |
53 | ))}
54 |
55 | )}
56 |
57 | )
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/Select.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("Select");
2 |
3 | let make =
4 | (~label=?, ~value=?, ~name=?, ~placeholder=?, ~color="teal", ~children) => {
5 | ...component,
6 | render: _self => {
7 | let labelComponent =
8 | switch label {
9 | | None => ReasonReact.nullElement
10 | | Some(label) => ReasonReact.stringToElement(label)
11 | };
12 |
13 | (
14 | labelComponent === ReasonReact.stringToElement("") ?
15 |
17 | labelComponent
18 | :
19 | ReasonReact.nullElement
20 | )
21 |
26 | (ReasonReact.arrayToElement(children))
27 |
28 |
39 |
;
40 | }
41 | };
42 |
43 | let default =
44 | ReasonReact.wrapReasonForJs(~component, jsProps =>
45 | make(
46 | ~label=?jsProps##label,
47 | ~value=?jsProps##value,
48 | ~name=?jsProps##name,
49 | ~placeholder=?jsProps##placeholder,
50 | ~color=jsProps##color,
51 | ~children=jsProps##children
52 | )
53 | );
54 |
--------------------------------------------------------------------------------
/src/components/TextArea.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("TextArea");
2 |
3 | let make =
4 | (
5 | ~label=?,
6 | ~value=?,
7 | ~htmlType=?,
8 | ~name=?,
9 | ~placeholder=?,
10 | ~color="teal",
11 | ~rows=?,
12 | ~hasError=false,
13 | _children
14 | ) => {
15 | ...component,
16 | render: _self => {
17 | let errorStyle =
18 | [
19 | Js.Boolean.to_js_boolean(hasError) === Js.Boolean.to_js_boolean(true) ?
20 | "border-red hover:border-red" :
21 | {j|border-grey-light hover:border-$(color)|j}
22 | ]
23 | |> String.concat("");
24 | let labelComponent =
25 | switch label {
26 | | None => ReasonReact.nullElement
27 | | Some(label) => ReasonReact.stringToElement(label)
28 | };
29 |
30 | (
31 | labelComponent === ReasonReact.stringToElement("") ?
32 |
34 | labelComponent
35 | :
36 | ReasonReact.nullElement
37 | )
38 |
46 |
;
47 | }
48 | };
49 |
50 | let default =
51 | ReasonReact.wrapReasonForJs(~component, jsProps =>
52 | make(
53 | ~label=?jsProps##label,
54 | ~value=?jsProps##value,
55 | ~htmlType=?jsProps##htmlType,
56 | ~name=?jsProps##name,
57 | ~placeholder=?jsProps##placeholder,
58 | ~color=jsProps##color,
59 | ~rows=?jsProps##rows,
60 | ~hasError=?jsProps##hasError,
61 | [||]
62 | )
63 | );
64 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import 'tailwindcss/dist/tailwind.min.css'
2 | import './styles/main.css'
3 |
4 | export { default as Alert } from './components/Alert.re'
5 | export { default as Article } from './components/Article.re'
6 | export { default as Box } from './components/Box.re'
7 | export { default as Breadcrumbs } from './components/Breadcrumbs'
8 | export { default as Button } from './components/Button.re'
9 | export { default as Card } from './components/Card.re'
10 | export { default as Checkbox } from './components/Checkbox.re'
11 | export { default as CodeBlock } from './components/CodeBlock.re'
12 | export { default as Collapsible } from './components/Collapsible.re'
13 | export { default as DangerousHTML } from './components/DangerousHTML.re'
14 | export { default as Dropdown } from './components/Dropdown.re'
15 | export { default as Flex } from './components/Flex.re'
16 | export { default as ImageCarousel } from './components/ImageCarousel'
17 | export { default as Loader } from './components/Loader.re'
18 | export { default as Margin } from './components/Margin.re'
19 | export { default as MegaFooter } from './components/MegaFooter'
20 | export { default as Modal } from './components/Modal.re'
21 | export { default as Navbar } from './components/Navbar.re'
22 | export { default as Padding } from './components/Padding.re'
23 | export { default as Panel } from './components/Panel.re'
24 | export { default as Portal } from './components/Portal'
25 | export { default as ProgressBar } from './components/ProgressBar.re'
26 | export { default as Radio } from './components/Radio'
27 | export { default as RenderIf } from './components/RenderIf.re'
28 | export { default as Select } from './components/Select.re'
29 | export { default as Tab } from './components/Tab'
30 | export { default as Table } from './components/Table'
31 | export { default as Tabs } from './components/Tabs'
32 | export { default as Text } from './components/Text.re'
33 | export { default as TextArea } from './components/TextArea.re'
34 | export { default as TextField } from './components/TextField.re'
35 | export { default as TruncateText } from './components/TruncateText'
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tails-ui",
3 | "version": "0.5.5",
4 | "description": "Clean UI based on tailwindcss",
5 | "author": "Tyler Knipfer ",
6 | "license": "MIT",
7 | "main": "dist/index.js",
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/knipferrc/tails-ui.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/knipferrc/tails-ui/issues"
14 | },
15 | "keywords": [
16 | "react",
17 | "reasonml",
18 | "tails-ui",
19 | "library",
20 | "components",
21 | "tailwindcss"
22 | ],
23 | "scripts": {
24 | "start": "rollup -c -w",
25 | "build": "NODE_ENV=production rollup -c",
26 | "precommit": "lint-staged",
27 | "prepublishOnly": "npm run build",
28 | "format:css": "prettier --write ./*.css ./**/*.css",
29 | "format:js": "prettier --write ./*.{js,json} ./**/*.{js,json}",
30 | "format:reason": "refmt --in-place src/**/*.re",
31 | "format": "npm run format:js && npm run format:css && npm run format:reason"
32 | },
33 | "devDependencies": {
34 | "@babel/core": "^7.0.0-beta.40",
35 | "@babel/preset-env": "^7.0.0-beta.40",
36 | "@babel/preset-react": "^7.0.0-beta.40",
37 | "@babel/preset-stage-0": "^7.0.0-beta.40",
38 | "bs-platform": "^2.2.1",
39 | "husky": "^0.14.3",
40 | "lint-staged": "^7.0.0",
41 | "prettier": "^1.10.2",
42 | "react": "^16.2.0",
43 | "react-dom": "^16.2.0",
44 | "reason-react": "^0.3.2",
45 | "rollup": "^0.56.3",
46 | "rollup-plugin-babel": "^4.0.0-beta.2",
47 | "rollup-plugin-bucklescript": "^0.6.1",
48 | "rollup-plugin-commonjs": "^8.3.0",
49 | "rollup-plugin-filesize": "^1.5.0",
50 | "rollup-plugin-node-resolve": "^3.0.3",
51 | "rollup-plugin-postcss": "^1.3.0",
52 | "rollup-plugin-replace": "^2.0.0",
53 | "rollup-plugin-uglify": "^3.0.0",
54 | "uglify-es": "^3.3.9"
55 | },
56 | "dependencies": {
57 | "tailwindcss": "^0.4.1"
58 | },
59 | "lint-staged": {
60 | "*.{js,json}": ["prettier --write", "git add"],
61 | "*.css": ["prettier --write", "git add"],
62 | "src/**/*.re": ["refmt --in-place", "git add"]
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/Collapsible.re:
--------------------------------------------------------------------------------
1 | type action =
2 | | Toggle;
3 |
4 | type state = {collapsed: bool};
5 |
6 | let component = ReasonReact.reducerComponent("Collapsible");
7 |
8 | let make = (~color="grey", ~title="Panel Title", ~children) => {
9 | ...component,
10 | initialState: () => {collapsed: false},
11 | reducer: (action, state) =>
12 | switch action {
13 | | Toggle => ReasonReact.Update({collapsed: ! state.collapsed})
14 | },
15 | render: self =>
16 |
17 |
19 |
20 | (ReasonReact.stringToElement(title))
21 |
22 |
self.send(Toggle)) className="cursor-pointer">
23 | (
24 | self.state.collapsed === false ?
25 |
26 |
29 |
32 | :
33 | ReasonReact.nullElement
34 | )
35 | (
36 | self.state.collapsed === true ?
37 |
38 |
41 |
44 | :
45 | ReasonReact.nullElement
46 | )
47 |
48 |
49 | (
50 | self.state.collapsed === false ?
51 |
(ReasonReact.arrayToElement(children))
:
52 | ReasonReact.nullElement
53 | )
54 |
55 | };
56 |
57 | let default =
58 | ReasonReact.wrapReasonForJs(~component, jsProps =>
59 | make(
60 | ~color=jsProps##color,
61 | ~title=jsProps##title,
62 | ~children=jsProps##children
63 | )
64 | );
65 |
--------------------------------------------------------------------------------
/src/components/Dropdown.re:
--------------------------------------------------------------------------------
1 | type action =
2 | | Toggle
3 | | Close;
4 |
5 | type state = {isOpen: bool};
6 |
7 | let component = ReasonReact.reducerComponent("Dropdown");
8 |
9 | let make = (~text="Dropdown", ~color="teal", ~children) => {
10 | ...component,
11 | initialState: () => {isOpen: false},
12 | reducer: (action, state) =>
13 | switch action {
14 | | Toggle => ReasonReact.Update({isOpen: ! state.isOpen})
15 | | Close => ReasonReact.Update({isOpen: false})
16 | },
17 | render: self =>
18 | self.send(Toggle))
21 | onBlur=(_event => self.send(Close))>
22 |
24 | (ReasonReact.stringToElement(text))
25 | (
26 | self.state.isOpen === false ?
27 |
28 |
31 |
34 | :
35 | ReasonReact.nullElement
36 | )
37 | (
38 | self.state.isOpen === true ?
39 |
40 |
43 |
46 | :
47 | ReasonReact.nullElement
48 | )
49 |
50 | (
51 | self.state.isOpen === true ?
52 |
53 |
54 | (ReasonReact.arrayToElement(children))
55 |
56 |
:
57 | ReasonReact.nullElement
58 | )
59 |
60 | };
61 |
62 | let default =
63 | ReasonReact.wrapReasonForJs(~component, jsProps =>
64 | make(
65 | ~text=jsProps##text,
66 | ~color=jsProps##color,
67 | ~children=jsProps##children
68 | )
69 | );
70 |
--------------------------------------------------------------------------------
/src/components/MegaFooter.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const MegaFooter = ({
4 | backgroundColor,
5 | fontColor,
6 | columnOne,
7 | columnTwo,
8 | columnThree,
9 | columnFour,
10 | copyrightText
11 | }) => (
12 |
13 |
14 |
15 |
16 | {columnOne && (
17 |
18 |
21 | {columnOne.header}
22 |
23 |
24 | {columnOne.links.map(link => (
25 |
29 | {React.cloneElement(link.value, {
30 | className: `text-${fontColor} hover:text-grey-light`
31 | })}
32 |
33 | ))}
34 |
35 |
36 | )}
37 | {columnTwo && (
38 |
39 |
42 | {columnTwo.header}
43 |
44 |
45 | {columnOne.links.map(link => (
46 |
50 | {React.cloneElement(link.value, {
51 | className: `text-${fontColor} hover:text-grey-light`
52 | })}
53 |
54 | ))}
55 |
56 |
57 | )}
58 | {columnThree && (
59 |
60 |
63 | {columnThree.header}
64 |
65 |
66 | {columnOne.links.map(link => (
67 |
71 | {React.cloneElement(link.value, {
72 | className: `text-${fontColor} hover:text-grey-light`
73 | })}
74 |
75 | ))}
76 |
77 |
78 | )}
79 | {columnFour && (
80 |
81 |
84 | {columnFour.header}
85 |
86 |
87 | {columnOne.links.map(link => (
88 |
92 | {React.cloneElement(link.value, {
93 | className: `text-${fontColor} hover:text-grey-light`
94 | })}
95 |
96 | ))}
97 |
98 |
99 | )}
100 |
101 |
102 |
103 | {copyrightText && (
104 |
107 | {copyrightText}
108 |
109 | )}
110 |
111 | )
112 |
113 | MegaFooter.defaultProps = {
114 | backgroundColor: 'blue',
115 | fontColor: 'white'
116 | }
117 |
118 | export default MegaFooter
119 |
--------------------------------------------------------------------------------
/src/components/Alert.re:
--------------------------------------------------------------------------------
1 | let component = ReasonReact.statelessComponent("Alert");
2 |
3 | let make =
4 | (
5 | ~color="teal",
6 | ~heading="Heading here",
7 | ~icon="info",
8 | ~message="A message",
9 | _children
10 | ) => {
11 | ...component,
12 | render: _self =>
13 |
14 |
15 |
16 | (
17 | icon === "warning" ?
18 |
22 |
25 |
28 | :
29 | ReasonReact.nullElement
30 | )
31 | (
32 | icon === "success" ?
33 |
37 |
40 |
43 | :
44 | ReasonReact.nullElement
45 | )
46 | (
47 | icon === "info" ?
48 |
52 |
55 |
58 | :
59 | ReasonReact.nullElement
60 | )
61 | (
62 | icon === "danger" ?
63 |
67 |
71 |
75 |
79 | :
80 | ReasonReact.nullElement
81 | )
82 |
83 |
84 |
85 |
86 | (ReasonReact.stringToElement(heading))
87 |
88 |
89 | (ReasonReact.stringToElement(message))
90 |
91 |
92 |
93 | };
94 |
95 | let default =
96 | ReasonReact.wrapReasonForJs(~component, jsProps =>
97 | make(
98 | ~color=jsProps##color,
99 | ~heading=jsProps##heading,
100 | ~icon=jsProps##icon,
101 | ~message=jsProps##message,
102 | [||]
103 | )
104 | );
105 |
--------------------------------------------------------------------------------