├── src ├── Helpers │ ├── Function.re │ ├── GenericHelpers.re │ └── StyleHelpers.re ├── Spacer.re ├── Empty.re ├── Label.re ├── UiTypes.re ├── Checkbox.re ├── Button.re ├── Alert.re ├── Lib.re ├── Notifications.re ├── Loader.re ├── Input.re ├── Textarea.re ├── Config.re ├── CheckboxStyles.re ├── Card.re ├── CardStyles.re ├── Segment.re ├── Dnd.re ├── InputStyles.re ├── AlertStyles.re ├── TabStyles.re ├── TextareaStyles.re ├── ButtonStyles.re ├── ActionDropdown.re ├── Notification.re ├── Slider.re ├── Dropdown.re ├── Tooltip.re └── Tabs.re ├── .prettierrc ├── postcss.config.js ├── jest.config.js ├── babel.config.js ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ui-components.yml ├── .gitignore ├── .storybook ├── preview-head.html └── main.js ├── scripts └── generate-react-icon-bindings.js ├── bsconfig.json ├── stories ├── loader.stories.js ├── typography.stories.js ├── alert.stories.js ├── tooltip.stories.js ├── checkbox.stories.js ├── icon.stories.js ├── segment.stories.js ├── input.stories.js ├── slider.stories.js ├── textarea.stories.js ├── button.stories.js ├── dropdown.stories.js ├── notifications.stories.js ├── card.stories.js └── tabs.stories.js ├── COPYING ├── __tests__ ├── GenericHelpers_test.re └── StyleHelpers_test.re ├── CHANGELOG.md ├── package.json └── README.md /src/Helpers/Function.re: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "print-width": 80, 3 | "trailingComma": "es5", 4 | "tabWidth": 4, 5 | "semi": false, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-flexbugs-fixes'), 4 | require('autoprefixer')({ 5 | flexbox: 'no-2009', 6 | }), 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transformIgnorePatterns: [ 3 | 'node_modules/(?!(' + 4 | '|bs-css' + 5 | '|@glennsl/bs-jest' + 6 | '|bs-css-emotion' + 7 | '|bs-platform' + 8 | ')/)', 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /src/Spacer.re: -------------------------------------------------------------------------------- 1 | module Vertical = { 2 | [@react.component] 3 | let make = (~amount: Css.Types.Length.t) => { 4 |
; 5 | }; 6 | }; 7 | module Horizontal = { 8 | [@react.component] 9 | let make = (~amount: Css.Types.Length.t) => { 10 |
; 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/Empty.re: -------------------------------------------------------------------------------- 1 | /* 2 | In some cases when pattern matching, we would want to show either a component, or nothing at all. 3 | The easiest way around this is to return an empty string as that won't render to anything. That 4 | isn't particularly declarative though, so we use this 'empty' component instead. 5 | */ 6 | 7 | [@react.component] 8 | let make = () => React.string(""); 9 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | ['@babel/plugin-proposal-private-property-in-object', { loose: true }], 4 | ], 5 | presets: [ 6 | [ 7 | '@babel/preset-env', 8 | { 9 | targets: { 10 | node: 'current', 11 | }, 12 | }, 13 | ], 14 | ], 15 | } 16 | -------------------------------------------------------------------------------- /src/Label.re: -------------------------------------------------------------------------------- 1 | open UiTypes; 2 | open InputStyles; 3 | 4 | [@react.component] 5 | let make = 6 | ( 7 | ~label=Unlabeled, 8 | ~identifier, 9 | ~variant=Normal, 10 | ~theme=Config.defaultTheme, 11 | ) => 12 | switch (label) { 13 | | Labeled(labelString) => 14 | 17 | | Unlabeled => 18 | }; 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### :notebook_with_decorative_cover: Description 2 | 3 | 4 | 5 | ### :memo: Checklist 6 | 7 | - [ ] All user-facing changes have changelog entries. 8 | - [ ] The PR description contains instructions for the reviewer, if necessary. 9 | 10 | ### :dart: Review Instructions 11 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -- general ------------------------------------------------------------------- 2 | .DS_Store 3 | .*sw? 4 | .cache 5 | 6 | # -- bucklescript -------------------------------------------------------------- 7 | .merlin 8 | .bsb.lock 9 | *.bs.js 10 | /lib/ 11 | 12 | # -- dependencies -------------------------------------------------------------- 13 | /node_modules/ 14 | /src/node_modules/ 15 | 16 | # -- generated ----------------------------------------------------------------- 17 | /storybook-static/ 18 | /bundleOutput/ 19 | /build/ 20 | /.out/ 21 | 22 | # -- logs ---------------------------------------------------------------------- 23 | npm-debug.log 24 | yarn-error.log 25 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/UiTypes.re: -------------------------------------------------------------------------------- 1 | [@bs.deriving accessors] 2 | type size = 3 | | Small 4 | | Medium 5 | | Large; 6 | 7 | [@bs.deriving accessors] 8 | type theme = 9 | | Light 10 | | Dark 11 | | TenzirBlue; 12 | 13 | [@bs.deriving accessors] 14 | type fontStyle = 15 | | Normal 16 | | Mono; 17 | 18 | type componentState = 19 | | Base 20 | | Active 21 | | Focus 22 | | Hovering; 23 | 24 | type percentOperations = 25 | | Add 26 | | Subtract; 27 | 28 | type changeTypes = 29 | | Lighten 30 | | Darken; 31 | 32 | [@bs.deriving accessors] 33 | type labels = 34 | | Labeled(string) 35 | | Unlabeled; 36 | 37 | [@bs.deriving accessors] 38 | type validation = 39 | | Valid 40 | | Invalid; 41 | -------------------------------------------------------------------------------- /scripts/generate-react-icon-bindings.js: -------------------------------------------------------------------------------- 1 | const icons = require("react-icons/fi"); 2 | const fs = require("fs"); 3 | const os = require("os"); 4 | 5 | const mappedIcons = Object.keys(icons).map(icon => { 6 | const header = "Icon;\n"; 7 | // Naively slice off two letters, just for removing 'Fi'; 8 | let nameForReason = icon.slice(2, icon.length); 9 | return `module ${nameForReason} = { 10 | [@bs.module "react-icons/fi"] [@react.component] 11 | external make: ( 12 | ~className: string=?, 13 | ~color: string=?, 14 | ~size: int=?, 15 | ~style: ReactDOMRe.Style.t=? 16 | ) => React.element = "${icon}"; 17 | }; 18 | `; 19 | }); 20 | 21 | return fs.writeFileSync(`./src/Icons.re`, mappedIcons.join("")); 22 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const { themes } = require('@storybook/theming') 2 | const { addParameters } = require('@storybook/react') 3 | 4 | module.exports = { 5 | addons: [ 6 | '@storybook/addon-actions/register', 7 | '@storybook/addon-postcss', 8 | '@storybook/addon-links/register', 9 | 'storybook-dark-mode/register', 10 | ], 11 | stories: ['../stories/*.stories.js'], 12 | options: { 13 | isFullscreen: false, 14 | showNav: true, 15 | showPanel: true, 16 | panelPosition: 'bottom', 17 | sidebarAnimations: true, 18 | enableShortcuts: false, 19 | isToolshown: true, 20 | storySort: undefined, 21 | configureJSX: true, 22 | }, 23 | } 24 | 25 | addParameters({ 26 | darkMode: { 27 | dark: { ...themes.dark, appBg: 'rgb(34, 38, 38)' }, 28 | light: themes.light, 29 | }, 30 | }) 31 | -------------------------------------------------------------------------------- /src/Checkbox.re: -------------------------------------------------------------------------------- 1 | open CheckboxStyles; 2 | open UiTypes; 3 | 4 | [@react.component] 5 | let make = 6 | ( 7 | ~defaultValue=false, 8 | ~disabled=false, 9 | ~label, 10 | ~id=?, 11 | ~width=[@reason.preserve_braces] 100.0, 12 | ~theme=Config.defaultTheme, 13 | ~placeholder="", 14 | ~validity=Valid, 15 | ~onChange=?, 16 | ) => { 17 | let identifier = 18 | switch (id) { 19 | | Some(idString) => idString 20 | | None => label 21 | }; 22 | 23 |
24 | 36 |
; 37 | }; 38 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tenzir-ui-component-library", 3 | "reason": { 4 | "react-jsx": 3 5 | }, 6 | "sources": [ 7 | { 8 | "dir": "src", 9 | "subdirs": true 10 | }, 11 | { 12 | "dir": "__tests__", 13 | "type": "dev" 14 | } 15 | ], 16 | "bsc-flags": ["-bs-super-errors", "-bs-no-version-header"], 17 | "warnings": { 18 | "number": "+A-4-41-48-102-107", 19 | "error": "+A-3-44-102-103" 20 | }, 21 | "package-specs": [ 22 | { 23 | "module": "es6-global", 24 | "in-source": true 25 | } 26 | ], 27 | "suffix": ".bs.js", 28 | "namespace": true, 29 | "bs-dependencies": [ 30 | "bs-flexboxgrid", 31 | "bs-webapi", 32 | "reason-react", 33 | "bs-css", 34 | "bs-css-emotion" 35 | ], 36 | "bs-dev-dependencies": ["@glennsl/bs-jest"], 37 | "refmt": 3 38 | } 39 | -------------------------------------------------------------------------------- /src/Button.re: -------------------------------------------------------------------------------- 1 | [@react.component] 2 | let make = 3 | ( 4 | ~disabled=false, 5 | ~type_="button", 6 | ~variant=ButtonStyles.Primary, 7 | ~size=UiTypes.Medium, 8 | ~theme=Config.defaultTheme, 9 | ~icon=?, 10 | ~iconPosition=ButtonStyles.Left, 11 | ~onClick=?, 12 | ~className="", 13 | ~children, 14 | ) => 15 | ; 31 | 32 | module Group = { 33 | [@react.component] 34 | let make = (~children, ~className="") => 35 |
36 | children 37 |
; 38 | }; 39 | -------------------------------------------------------------------------------- /src/Alert.re: -------------------------------------------------------------------------------- 1 | open AlertStyles; 2 | 3 | [@react.component] 4 | let make = 5 | ( 6 | ~children=?, 7 | ~variant=Primary, 8 | ~theme=Config.defaultTheme, 9 | ~size=UiTypes.Medium, 10 | ~className="", 11 | ~message=?, 12 | ) => 13 |

15 | <> 16 | 17 | {switch (variant) { 18 | | Primary 19 | | Secondary => 20 | | Success => 21 | | Warning => 22 | | Danger => 23 | }} 24 | 25 | {switch (message, children) { 26 | | (Some(message), Some(children)) => 27 | <> message->React.string children 28 | | (Some(message), None) => message->React.string 29 | | (None, Some(children)) => children 30 | | (None, None) => 31 | }} 32 | 33 |

; 34 | -------------------------------------------------------------------------------- /src/Lib.re: -------------------------------------------------------------------------------- 1 | module Function = { 2 | let id = x => x; 3 | let apply = (x, fn) => fn(x); 4 | let applyF = (fn, x) => fn(x); 5 | let const = (x, _) => x; 6 | let compose = (f, g, x) => f(g(x)); 7 | let pipe = (g, f, x) => f(g(x)); 8 | 9 | module Infix = { 10 | let (>>) = pipe; 11 | let (<<) = compose; 12 | }; 13 | }; 14 | 15 | module Jsx = { 16 | open Webapi; 17 | open Function.Infix; 18 | let focus = (ref: ReactDOM.Ref.currentDomRef) => 19 | ref.current 20 | ->Js.Nullable.toOption 21 | ->Belt.Option.map( 22 | Dom.Element.unsafeAsHtmlElement >> Dom.HtmlElement.focus, 23 | ) 24 | ->ignore; 25 | 26 | module Infix = { 27 | let (<&&>) = (a, b) => a ? b : React.null; 28 | }; 29 | }; 30 | 31 | module Event = { 32 | let getValueFromEvent = event => ReactEvent.Synthetic.target(event)##value; 33 | module Keyboard = { 34 | external toKeyboardEvent: 'a => Webapi.Dom.KeyboardEvent.t = "%identity"; 35 | 36 | let keyWas = (inputKey, e) => 37 | e 38 | |> toKeyboardEvent 39 | |> Webapi.Dom.KeyboardEvent.key 40 | |> (eventKey => eventKey === inputKey); 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /src/Notifications.re: -------------------------------------------------------------------------------- 1 | open BsFlexboxgrid; 2 | 3 | module Styles = { 4 | open Css; 5 | let container = 6 | style([ 7 | position(`fixed), 8 | width(100.0->vw), 9 | height(100.0->vh), 10 | left(`zero), 11 | top(`zero), 12 | pointerEvents(`none), 13 | ]); 14 | }; 15 | 16 | [@react.component] 17 | let make = 18 | ( 19 | ~theme, 20 | ~notifications: array(Notification.t), 21 | ~handleDismissal=_ => (), 22 | ~defaultAnimationTime=400, 23 | ~maxAmount=5, 24 | ) => { 25 | 26 | 27 | 28 | {notifications 29 | ->Belt.Array.reverse 30 | ->Belt.Array.slice(~offset=0, ~len=maxAmount) 31 | ->Belt.Array.reverse 32 | ->Belt.Array.map(notification => { 33 | 40 | }) 41 | ->React.array} 42 | 43 | 44 | ; 45 | }; 46 | -------------------------------------------------------------------------------- /src/Loader.re: -------------------------------------------------------------------------------- 1 | open Css; 2 | let pulse = 3 | keyframes([ 4 | (0, [opacity(0.1)]), 5 | (60, [opacity(0.5)]), 6 | (100, [opacity(0.1)]), 7 | ]); 8 | 9 | let loaderStyles = 10 | style([ 11 | display(`flex), 12 | alignSelf(`center), 13 | justifyContent(`center), 14 | height(100.0->pct), 15 | width(100.0->pct), 16 | selector( 17 | "& circle", 18 | [ 19 | opacity(0.1), 20 | animationName(pulse), 21 | animationDuration(1200), 22 | animationTimingFunction(`easeInOut), 23 | animationIterationCount(`infinite), 24 | ], 25 | ), 26 | selector("& circle:nth-of-type(1)", [animationDelay(100)]), 27 | selector("& circle:nth-of-type(2)", [animationDelay(200)]), 28 | ]); 29 | 30 | type background = 31 | | Dark 32 | | Light; 33 | 34 | [@react.component] 35 | let make = (~theme=Config.defaultTheme, ~className="") => { 36 | let colors = StyleHelpers.colorsFromThemeVariant(theme); 37 | 38 |
39 | Css_AtomicTypes.Color.toString} 41 | size=40 42 | /> 43 |
; 44 | }; 45 | 46 | let default = make; 47 | -------------------------------------------------------------------------------- /stories/loader.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 3 | import { okaidia } from "react-syntax-highlighter/dist/esm/styles/prism"; 4 | import { useDarkMode } from "storybook-dark-mode"; 5 | import { make as Loader } from "../src/Loader.bs.js"; 6 | import { make as Card } from "../src/Card.bs.js"; 7 | import { light, dark } from "../src/UiTypes.bs"; 8 | export default { 9 | title: "Loader", 10 | }; 11 | 12 | const margin = { 13 | margin: "1rem", 14 | }; 15 | export const Loaders = () => { 16 | const theme = useDarkMode() ? dark : light; 17 | return ( 18 | <> 19 |
20 | 21 |

Loader

22 |

Interface

23 | 24 | {`type Loader: ( 25 | ~className: option(string), /* Defaults to empty string */ 26 | ~theme: option(UiTypes.theme) 27 | ) => React.element; 28 | `} 29 | 30 | 31 |

Preview

32 | 33 | 34 | 35 |
36 |
37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/Helpers/GenericHelpers.re: -------------------------------------------------------------------------------- 1 | open UiTypes; 2 | 3 | /* 4 | * Bound a value within its upper and lower bounds. Works for int / float 5 | */ 6 | let bound = (lowerBound, upperBound, value) => 7 | switch (value) { 8 | | value when value > upperBound => upperBound 9 | | value when value < lowerBound => lowerBound 10 | | _ => value 11 | }; 12 | 13 | /* 14 | * This function is used to add or subtract a percentate from any color 15 | * value. We take the percentage relative to 255 (max rgb value for any 16 | * given channel) and either add or subtract it. 17 | */ 18 | let operatePercent = (percentage, operation, value) => { 19 | let amount = 255 * percentage / 100; 20 | switch (operation) { 21 | | Add => value + amount 22 | | Subtract => value - amount 23 | }; 24 | }; 25 | 26 | /* 27 | * When not providing an id for inputs with labels, we would still need to generate one. 28 | * We do this by re-using the labelstring in this case. There could be duplicates but this 29 | * will be caught by React and give a console output error. However, in 90% of the cases, 30 | * falling back to the label string will be just fine. 31 | */ 32 | let genIdentifier = (id, label) => 33 | switch (id, label) { 34 | | (Some(idString), _) => idString 35 | | (None, Labeled(labelString)) => labelString 36 | | (None, Unlabeled) => "" 37 | }; 38 | 39 | /* 40 | * To aid the creation of random id's, we use UUID's v4 fn. 41 | */ 42 | [@bs.module "uuid"] [@bs.val] external v4: unit => string = "v4"; 43 | -------------------------------------------------------------------------------- /src/Input.re: -------------------------------------------------------------------------------- 1 | open UiTypes; 2 | open InputStyles; 3 | 4 | [@react.component] 5 | let make = 6 | ( 7 | ~_type="text", 8 | ~defaultValue="", 9 | ~className="", 10 | ~value=?, 11 | ~disabled=false, 12 | ~label=Unlabeled, 13 | ~id=?, 14 | ~validity=Valid, 15 | ~variant=Normal, 16 | ~width=[@reason.preserve_braces] 100.0, 17 | ~theme=Config.defaultTheme, 18 | ~placeholder="", 19 | ~onChange=?, 20 | ~onKeyDown=?, 21 | ~onBlur=?, 22 | ) => { 23 | let identifier = GenericHelpers.genIdentifier(id, label); 24 | 25 |
26 |
; 58 | }; 59 | -------------------------------------------------------------------------------- /stories/typography.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 3 | import { okaidia } from 'react-syntax-highlighter/dist/esm/styles/prism' 4 | import { useDarkMode } from 'storybook-dark-mode' 5 | import { make as Card } from '../src/Card.bs.js' 6 | import { light, dark } from '../src/UiTypes.bs' 7 | 8 | export default { 9 | title: 'Typography', 10 | } 11 | 12 | const margin = { 13 | margin: '1rem', 14 | } 15 | 16 | export const typograph = () => { 17 | const theme = useDarkMode() ? dark : light 18 | return ( 19 | <> 20 |
21 | 22 |

Typography

23 |

This is an h1 title

24 |

This is an h2 title

25 |

This is an h3 title

26 |

This is an h4 title

27 |
This is an h5 title
28 |
This is an h6 title
29 |

Here is the paragraph text

30 |
    31 |
  • I'm in an unordered list
  • 32 |
  • I'm in an unordered list
  • 33 |
34 |
    35 |
  1. I'm in an ordered list
  2. 36 |
  3. I'm in an ordered list
  4. 37 |
38 |
39 |
40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Tenzir GmbH 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /src/Textarea.re: -------------------------------------------------------------------------------- 1 | open UiTypes; 2 | open TextareaStyles; 3 | 4 | [@react.component] 5 | let make = 6 | ( 7 | ~defaultValue="", 8 | ~value=?, 9 | ~label=Unlabeled, 10 | ~disabled=false, 11 | ~id=?, 12 | ~variant=Normal, 13 | ~validity=Valid, 14 | ~width=[@reason.preserve_braces] 100.0, 15 | ~resize=NoResize, 16 | ~theme=Config.defaultTheme, 17 | ~placeholder="", 18 | ~onChange=?, 19 | ~onBlur=?, 20 | ~styles=[], 21 | ~rows=4, 22 | ~cols=50, 23 | ) => { 24 | let identifier = GenericHelpers.genIdentifier(id, label); 25 | 26 |
27 |