├── .editorconfig ├── .envrc ├── .eslintignore ├── .eslintrc.yml ├── .gitattributes ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ROADMAP.md ├── bower.json ├── docs ├── .eslintrc.yml ├── App.jsx ├── AppSetup.purs ├── Example.purs ├── Examples │ ├── Badge.example.purs │ ├── Border.example.purs │ ├── Breadcrumb.example.purs │ ├── Button.example.purs │ ├── ButtonGroup.example.purs │ ├── Card.example.purs │ ├── CardGrid.example.purs │ ├── Color.example.purs │ ├── Column.example.purs │ ├── Details.example.purs │ ├── Divider.example.purs │ ├── DropdownButton.example.purs │ ├── EditableTable.example.purs │ ├── FixedPrecisionInput.example.purs │ ├── Form.example.purs │ ├── Icon.example.purs │ ├── Images.example.purs │ ├── Input.example.purs │ ├── InputGroup.example.purs │ ├── LabeledField.example.purs │ ├── Layouts.example.purs │ ├── Link.example.purs │ ├── List.example.purs │ ├── Loader.example.purs │ ├── Lockup.example.purs │ ├── Modal.example.purs │ ├── NativeSelect.example.purs │ ├── Navigation.example.purs │ ├── Pagination.example.purs │ ├── Pill.example.purs │ ├── Progress.example.purs │ ├── Responsive.example.purs │ ├── Row.example.purs │ ├── ScrollManager.example.purs │ ├── Select.example.purs │ ├── Slider.example.purs │ ├── Spacing.example.purs │ ├── StatusSlat.example.purs │ ├── Svg.example.purs │ ├── Tab.example.purs │ ├── Table.example.purs │ ├── Text.example.purs │ ├── Textarea.example.purs │ ├── Toast.example.purs │ ├── Tooltip.example.purs │ ├── Upload.example.purs │ └── Wizard.example.purs ├── Examples2 │ ├── Box.example.purs │ ├── Button.example.purs │ ├── ButtonGroup.example.purs │ ├── Clip.example.purs │ ├── Image.example.purs │ ├── Link.example.purs │ ├── QRCode.example.purs │ ├── Slat.example.purs │ └── Text.example.purs ├── MUIColorScheme.purs ├── ScrollManager.jsx ├── Utility │ ├── ReactRouter.js │ └── ReactRouter.purs ├── index.html └── index.jsx ├── flake.lock ├── flake.nix ├── package-lock.json ├── package.json ├── packages.dhall ├── spago.dhall ├── src ├── JSS │ ├── JSS.js │ └── JSS.purs └── Lumi │ ├── Components.purs │ ├── Components │ ├── Badge.purs │ ├── Border.purs │ ├── Breadcrumb.purs │ ├── Button.purs │ ├── ButtonGroup.purs │ ├── Card.purs │ ├── CardGrid.purs │ ├── Color.purs │ ├── Column.purs │ ├── Details.purs │ ├── Divider.purs │ ├── DropdownButton.js │ ├── DropdownButton.purs │ ├── EditableTable.purs │ ├── FetchCache.purs │ ├── FixedPrecisionInput.js │ ├── FixedPrecisionInput.purs │ ├── Form.purs │ ├── Form │ │ ├── Defaults.purs │ │ ├── Internal.purs │ │ ├── Table.purs │ │ └── Validation.purs │ ├── Icon.purs │ ├── Images.purs │ ├── Input.js │ ├── Input.purs │ ├── InputGroup.purs │ ├── LabeledField.purs │ ├── Layouts.purs │ ├── Layouts │ │ ├── Centered.purs │ │ ├── OneColumnWithHeader.purs │ │ └── Tabs.purs │ ├── Link.purs │ ├── List.purs │ ├── Loader.purs │ ├── Lockup.purs │ ├── Modal.js │ ├── Modal.purs │ ├── NativeSelect.purs │ ├── Navigation.js │ ├── Navigation.purs │ ├── Orientation.purs │ ├── Pagination.purs │ ├── Pill.purs │ ├── Progress.purs │ ├── Responsive.js │ ├── Responsive.purs │ ├── Row.purs │ ├── Select.purs │ ├── Select │ │ └── SelectBackend.purs │ ├── Size.purs │ ├── Slider.purs │ ├── Spacing.purs │ ├── Status.purs │ ├── StatusSlat.purs │ ├── Styles.purs │ ├── Svg.purs │ ├── Tab.js │ ├── Tab.purs │ ├── Table.js │ ├── Table.purs │ ├── Table │ │ └── FilterDropdown.purs │ ├── Text.purs │ ├── Textarea.purs │ ├── Toast.purs │ ├── Tooltip.purs │ ├── Upload.js │ ├── Upload.purs │ ├── Wizard.purs │ └── ZIndex.purs │ ├── Components2 │ ├── Box.purs │ ├── Button.purs │ ├── ButtonGroup.purs │ ├── Clip.js │ ├── Clip.purs │ ├── Image.purs │ ├── Link.purs │ ├── PasswordStrength.js │ ├── PasswordStrength.purs │ ├── QRCode.js │ ├── QRCode.purs │ ├── ScrollObserver.js │ ├── ScrollObserver.purs │ ├── Slat.purs │ ├── Tabs.purs │ └── Text.purs │ ├── Styles.purs │ └── Styles │ ├── Border.purs │ ├── Box.purs │ ├── Clip.purs │ ├── Link.purs │ ├── Loader.purs │ ├── Responsive.purs │ ├── Slat.purs │ └── Theme.purs └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Unix-style newlines with a newline ending every file 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | 11 | [Makefile] 12 | indent_style = tab 13 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack.config.js 2 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | commonjs: true 4 | es6: true 5 | node: true 6 | extends: 7 | - eslint:recommended 8 | # - plugin:prettier/recommended 9 | globals: 10 | Atomics: readonly 11 | SharedArrayBuffer: readonly 12 | parserOptions: 13 | ecmaFeatures: 14 | jsx: false 15 | ecmaVersion: 2018 16 | sourceType: module 17 | rules: 18 | strict: 19 | - error 20 | - global 21 | no-unused-vars: 22 | - error 23 | - argsIgnorePattern: ^_ 24 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json binary 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules/ 5 | /bower_components/ 6 | /.psc-package/ 7 | .spago 8 | 9 | # testing 10 | /coverage 11 | 12 | # purescript 13 | /output/ 14 | /.pulp-cache/ 15 | /generated-docs/ 16 | /.psc* 17 | /.purs* 18 | /.psa* 19 | 20 | # production build 21 | /build/ 22 | 23 | # misc 24 | .DS_Store 25 | .vscode 26 | .env.local 27 | .env.development.local 28 | .env.test.local 29 | .env.production.local 30 | .direnv 31 | 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | bower.json 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "trailingComma": "none" 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: node_js 4 | node_js: 10 5 | 6 | install: 7 | - npm install 8 | 9 | script: 10 | - npx bower install 11 | - npm run build 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome, particularly for bug fixes or suggestions, but we want to be clear upfront that this library tool specific to Lumi's needs. We may not take contributions we don't have a use for, especially if they add third party dependencies or cause breaking API changes we'll need to account for. Please ask/propose in an issue if you're unsure. 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purescript-lumi-components [![Build Status](https://travis-ci.org/lumihq/purescript-lumi-components.svg?branch=master)](https://travis-ci.org/lumihq/purescript-lumi-components) 2 | 3 | - [lumihq.github.io/purescript-lumi-components](https://lumihq.github.io/purescript-lumi-components/) 4 | - [purescript-lumi-components on Pursuit](https://pursuit.purescript.org/packages/purescript-lumi-components/) 5 | 6 | This is a component library focused on Lumi's specific UI and UX needs. Available components are found in `src/components`. 7 | 8 | ## Goals and Roadmap 9 | 10 | See [ROADMAP.md](ROADMAP.md) for more info. 11 | 12 | ## Installation 13 | 14 | ```sh 15 | bower i -S purescript-lumi-components 16 | ``` 17 | 18 | To use the styles that come with these components the CSS needs to be injected into the page. The easiest way to do this is to run the `attachGlobalComponentStyles` effect available in `Lumi.Components.Styles` one time as your application initializes. 19 | 20 | You will also need a few `npm` dependencies. These dependencies and their versions must be compatible with the ones listed [here](https://github.com/lumihq/purescript-lumi-components/blob/master/package.json#L31). 21 | 22 | ## Local development 23 | 24 | ```sh 25 | npm i; npx bower i; npx pulp build 26 | npm start 27 | ``` 28 | 29 | You can run production builds (output minified, static files to `build/`) using `npm run build`. 30 | 31 | 32 | ## Tagging a new release 33 | 34 | ```sh 35 | pulp version 0.x.y 36 | pulp publish 37 | npm run deploy 38 | ``` 39 | 40 | _Don't use `npm version`!_ 41 | 42 | ## Contributing 43 | 44 | See [CONTRIBUTING.md](CONTRIBUTING.md) for more info. 45 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Goals and Roadmap 2 | 3 | Lumi Components aims to encapsulate the low level design patterns and UI building blocks used at Lumi. This library can be thought of as a DSL for constructing business-specific components which follow Lumi's style guidelines. This idea will continue to influence the components we create and the way we structure their APIs. 4 | 5 | For example, an `Input` component which just wraps an HTML input and passes through props like `className` and `style` is too low-level. It requires too much knowledge of both CSS and our style rules (such as padding in multiples of 8 pixels) to use. This slows initial development of new pages, clutters up the code, and creates inconsistencies in the UI. 6 | 7 | "But I see those props on many of these components!" 8 | 9 | Yes, this is definitely a work in progress. While this is a rough outline of our goal for a 1.0 release, we're still working out the best ways to accomplish these goals. Short term plans include: 10 | 11 | - remove the use of `Nullable` 12 | - these APIs are a remnant of having ported some of these components from JavaScript 13 | - remove `String` props where possible 14 | - props like `variant` should be full enums or removed and the component split into separate more specialized components 15 | - `content`, `child`, `text`, etc, props are generally better as `JSX` than strings because it's just about as opaque while being flexible enough to allow interesting component composition 16 | - remove global CSS 17 | - the CSS should generate unique class names to prevent possible conflicts with UI elements that aren't a part of this library 18 | - should be more type safe, both in style definition and the attaching of styles to components 19 | 20 | Longer term plans may include more DSLs similar to `Form` for use cases such as layout, or the ability to import the component library into Sketch. 21 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-lumi-components", 3 | "license": [ 4 | "Apache-2.0" 5 | ], 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/lumihq/purescript-lumi-components" 9 | }, 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "output" 15 | ], 16 | "dependencies": { 17 | "purescript-aff": "^v7.1.0", 18 | "purescript-aff-coroutines": "^v9.0.0", 19 | "purescript-arrays": "^v7.2.1", 20 | "purescript-bifunctors": "^v6.0.0", 21 | "purescript-colors": "^v7.0.1", 22 | "purescript-console": "^v6.0.0", 23 | "purescript-control": "^v6.0.0", 24 | "purescript-coroutines": "^v7.0.0", 25 | "purescript-datetime": "^v6.1.0", 26 | "purescript-effect": "^v4.0.0", 27 | "purescript-either": "^v6.1.0", 28 | "purescript-enums": "^v6.0.1", 29 | "purescript-exceptions": "^v6.0.0", 30 | "purescript-fixed-precision": "^v5.0.0", 31 | "purescript-foldable-traversable": "^v6.0.0", 32 | "purescript-foreign": "^v7.0.0", 33 | "purescript-foreign-object": "^v4.1.0", 34 | "purescript-free": "^v7.0.0", 35 | "purescript-heterogeneous": "^v0.6.0", 36 | "purescript-integers": "^v6.0.0", 37 | "purescript-js-timers": "^v6.1.0", 38 | "purescript-js-uri": "https://github.com/purescript-contrib/purescript-js-uri.git#v3.1.0", 39 | "purescript-maybe": "^v6.0.0", 40 | "purescript-media-types": "^v6.0.0", 41 | "purescript-newtype": "^v5.0.0", 42 | "purescript-nonempty": "^v7.0.0", 43 | "purescript-nullable": "^v6.0.0", 44 | "purescript-numbers": "^v9.0.1", 45 | "purescript-ordered-collections": "^v3.0.0", 46 | "purescript-parallel": "^v6.0.0", 47 | "purescript-partial": "^v4.0.0", 48 | "purescript-prelude": "^v6.0.1", 49 | "purescript-profunctor-lenses": "^v8.0.0", 50 | "purescript-react-basic": "^v17.0.0", 51 | "purescript-react-basic-classic": "https://github.com/lumihq/purescript-react-basic-classic.git#v3.0.0", 52 | "purescript-react-basic-dnd": "https://github.com/lumihq/purescript-react-dnd-basic.git#v10.1.0", 53 | "purescript-react-basic-dom": "https://github.com/lumihq/purescript-react-basic-dom.git#v6.1.0", 54 | "purescript-react-basic-emotion": "^v7.1.0", 55 | "purescript-react-basic-hooks": "^v8.2.0", 56 | "purescript-record": "^v4.0.0", 57 | "purescript-refs": "^v6.0.0", 58 | "purescript-simple-json": "^v9.0.0", 59 | "purescript-st": "^v6.2.0", 60 | "purescript-strings": "^v6.0.1", 61 | "purescript-tailrec": "^v6.1.0", 62 | "purescript-transformers": "^v6.0.0", 63 | "purescript-tuples": "^v7.0.0", 64 | "purescript-unsafe-coerce": "^v6.0.0", 65 | "purescript-unsafe-reference": "^v5.0.0", 66 | "purescript-web-dom": "^v6.0.0", 67 | "purescript-web-events": "^v4.0.0", 68 | "purescript-web-file": "^v4.0.0", 69 | "purescript-web-html": "^v4.1.0", 70 | "purescript-web-storage": "^v5.0.0", 71 | "purescript-web-uievents": "^v4.0.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /docs/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - plugin:react/recommended 3 | parser: babel-eslint 4 | parserOptions: 5 | sourceType: module 6 | ecmaFeatures: 7 | jsx: true 8 | settings: 9 | react: 10 | version: detect 11 | rules: 12 | react/prop-types: 0 13 | -------------------------------------------------------------------------------- /docs/AppSetup.purs: -------------------------------------------------------------------------------- 1 | module AppSetup where 2 | 3 | import React.Basic.Classic (JSX) 4 | import React.Basic.ReactDND (dndProvider) 5 | import React.Basic.ReactDND.Backends.HTML5Backend (html5Backend) 6 | 7 | dragDropContext :: JSX -> JSX 8 | dragDropContext = dndProvider html5Backend 9 | -------------------------------------------------------------------------------- /docs/Examples/Badge.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Badge where 2 | 3 | import Lumi.Components.Badge (badge, badge_) 4 | import Lumi.Components.Color (colors) 5 | import Lumi.Components.Column (column_) 6 | import Lumi.Components.Spacing (Space(..), vspace) 7 | import Lumi.Components.Text (nbsp) 8 | import React.Basic.Classic (JSX) 9 | import React.Basic.DOM as R 10 | 11 | docs :: JSX 12 | docs = 13 | column_ 14 | [ badge_ "1" 15 | , vspace S8 16 | 17 | , badge_ "2" 18 | , vspace S8 19 | 20 | , badge_ "3" 21 | , vspace S8 22 | 23 | , badge_ "Hello!" 24 | , vspace S8 25 | 26 | , badge 27 | { background: colors.primary 28 | , color: colors.white 29 | , style: R.css {} 30 | , text: "1" 31 | } 32 | , vspace S8 33 | 34 | , badge 35 | { background: colors.primary 36 | , color: colors.white 37 | , style: R.css {} 38 | , text: nbsp 39 | } 40 | ] 41 | -------------------------------------------------------------------------------- /docs/Examples/Border.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Border where 2 | 3 | import Prelude 4 | 5 | import Lumi.Components.Border (borderBottom, borderRound, borderSquare, borderSquareBottom, borderSquareTop, borderTop) 6 | import Lumi.Components.Column (column, column_) 7 | import Lumi.Components.Example (example) 8 | import Lumi.Components.Text (h2_) 9 | import React.Basic.Classic (JSX) 10 | import React.Basic.DOM as R 11 | 12 | docs :: JSX 13 | docs = 14 | column_ 15 | [ h2_ "Rounded borders" 16 | , example 17 | $ borderRound $ 18 | column 19 | { children: [ R.text "bordered element" ] 20 | , style: R.css {} 21 | } 22 | , h2_ "Square borders" 23 | , example 24 | $ borderSquare $ 25 | column 26 | { children: [ R.text "bordered element" ] 27 | , style: R.css {} 28 | } 29 | , h2_ "Square Top borders" 30 | , example 31 | $ borderSquareTop $ 32 | column 33 | { children: [ R.text "bordered element" ] 34 | , style: R.css {} 35 | } 36 | , h2_ "Square Bottom borders" 37 | , example 38 | $ borderSquareBottom $ 39 | column 40 | { children: [ R.text "bordered element" ] 41 | , style: R.css {} 42 | } 43 | , h2_ "Top borders" 44 | , example 45 | $ borderTop $ 46 | column 47 | { children: [ R.text "bordered element" ] 48 | , style: R.css {} 49 | } 50 | , h2_ "Bottom borders" 51 | , example 52 | $ borderBottom $ 53 | column 54 | { children: [ R.text "bordered element" ] 55 | , style: R.css {} 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /docs/Examples/Breadcrumb.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Breadcrumb where 2 | 3 | import Prelude 4 | 5 | import Data.Array.NonEmpty (cons') 6 | import Data.Maybe (Maybe(..)) 7 | import Data.Nullable (toNullable) 8 | import Lumi.Components.Breadcrumb (breadcrumb, compactBreadcrumb) 9 | import Lumi.Components.Column (column_) 10 | import Lumi.Components.Text (h2_) 11 | import Lumi.Components.Example (example) 12 | import React.Basic.Classic (JSX) 13 | import Web.HTML.History (URL(..)) 14 | 15 | docs :: JSX 16 | docs = 17 | column_ 18 | [ example 19 | $ breadcrumb 20 | { onNavigate: toNullable Nothing 21 | , items: cons' 22 | { label: "Lorem", href: URL "#/input" } 23 | [ { label: "Ipsum", href: URL "#/link" } 24 | , { label: "Dolor", href: URL "" } 25 | ] 26 | } 27 | 28 | , h2_ "Compact variation" 29 | , example 30 | $ compactBreadcrumb 31 | { onNavigate: toNullable Nothing 32 | , items: cons' 33 | { label: "Lorem", href: URL "#/input" } 34 | [ { label: "Ipsum", href: URL "#/link" } 35 | ] 36 | } 37 | 38 | , example 39 | $ compactBreadcrumb 40 | { onNavigate: toNullable Nothing 41 | , items: cons' 42 | { label: "Lorem", href: URL "#/input" } 43 | [] 44 | } 45 | ] 46 | -------------------------------------------------------------------------------- /docs/Examples/ButtonGroup.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.ButtonGroup where 2 | 3 | import Prelude 4 | 5 | import Effect.Console (log) 6 | import Effect.Uncurried (mkEffectFn1) 7 | import Lumi.Components.Button (button, primary, secondary) 8 | import Lumi.Components.ButtonGroup (buttonGroup) 9 | import Lumi.Components.Column (column_) 10 | import Lumi.Components.NativeSelect (nativeSelect, defaults) 11 | import Lumi.Components.Text (h2_) 12 | import Lumi.Components.Example (example) 13 | import React.Basic.Classic (JSX) 14 | import React.Basic.DOM as R 15 | 16 | docs :: JSX 17 | docs = 18 | column_ 19 | [ h2_ "Not Joined" 20 | , example 21 | $ buttonGroup 22 | { joined: false 23 | , style: R.css {} 24 | , children: 25 | [ button primary { title = "Button" } 26 | , button secondary { title = "Button" } 27 | ] 28 | } 29 | , h2_ "Not Joined" 30 | , example 31 | $ buttonGroup 32 | { joined: false 33 | , style: R.css {} 34 | , children: 35 | [ button primary { title = "Button" } 36 | , button secondary { title = "Button" } 37 | , button secondary { title = "Button" } 38 | ] 39 | } 40 | , h2_ "Not Joined" 41 | , example 42 | $ buttonGroup 43 | { joined: false 44 | , style: R.css {} 45 | , children: 46 | [ button primary { title = "Button" } 47 | , nativeSelect defaults 48 | { options = [] 49 | , onChange = mkEffectFn1 \_ -> log "onChange" 50 | , value = "Foo bar" 51 | } 52 | , button secondary { title = "Button" } 53 | ] 54 | } 55 | , h2_ "Joined" 56 | , example 57 | $ buttonGroup 58 | { joined: true 59 | , style: R.css {} 60 | , children: 61 | [ button secondary { title = "Button" } 62 | , button secondary { title = "Button" } 63 | ] 64 | } 65 | , h2_ "Joined" 66 | , example 67 | $ buttonGroup 68 | { joined: true 69 | , style: R.css {} 70 | , children: 71 | [ button secondary { title = "Button" } 72 | , button secondary { title = "Button" } 73 | , button secondary { title = "Button" } 74 | ] 75 | } 76 | ] 77 | -------------------------------------------------------------------------------- /docs/Examples/Card.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Card where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Data.Newtype (un) 7 | import Effect.Console (log) 8 | import Lumi.Components.Card (card, defaults) 9 | import Lumi.Components.Column (column_) 10 | import Lumi.Components.Row (row_) 11 | import Lumi.Components.Spacing (Space(..), hspace) 12 | import Lumi.Components.Text (h2_) 13 | import Lumi.Components.Example (example) 14 | import React.Basic.Classic (JSX) 15 | import React.Basic.DOM as R 16 | import Web.HTML.History (URL(..)) 17 | 18 | docs :: JSX 19 | docs = 20 | row_ 21 | [ column_ 22 | [ h2_ "Default state" 23 | , example $ 24 | card defaults 25 | { title = "Poly Mailers" 26 | , subtitle = "14.50\" × 19.00\"" 27 | , href = Just $ URL "https://www.lumi.com/" 28 | , onNavigate = log <<< un URL 29 | , children = 30 | [ R.img 31 | { src: "https://s3.amazonaws.com/lumi-flapjack-production/mockups/4985252ac5ba4c79e2ecbc6d3438e4ca.jpg" 32 | } 33 | ] 34 | } 35 | ] 36 | 37 | , hspace S32 38 | 39 | , column_ 40 | [ h2_ "Selected state" 41 | , example $ 42 | card defaults 43 | { title = "Poly Mailers" 44 | , subtitle = "14.50\" × 19.00\"" 45 | , selected = true 46 | , children = 47 | [ R.img 48 | { src: "https://s3.amazonaws.com/lumi-flapjack-production/mockups/4985252ac5ba4c79e2ecbc6d3438e4ca.jpg" 49 | } 50 | ] 51 | } 52 | ] 53 | ] 54 | -------------------------------------------------------------------------------- /docs/Examples/Column.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Column where 2 | 3 | import Prelude 4 | 5 | import Lumi.Components.Button (button, primary) 6 | import Lumi.Components.Column (column_, responsiveColumn_) 7 | import Lumi.Components.Text (h2_, p_) 8 | import Lumi.Components.Example (example) 9 | import React.Basic.Classic (JSX) 10 | 11 | docs :: JSX 12 | docs = 13 | column_ 14 | [ example 15 | $ column_ 16 | [ button primary { title = "Button" } 17 | , button primary { title = "Button" } 18 | , button primary { title = "Button" } 19 | ] 20 | 21 | , h2_ "Responsive column" 22 | , p_ "* Resize the window to see how the component responds." 23 | , example 24 | $ responsiveColumn_ 25 | [ button primary { title = "Button" } 26 | , button primary { title = "Button" } 27 | , button primary { title = "Button" } 28 | ] 29 | ] 30 | -------------------------------------------------------------------------------- /docs/Examples/Details.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Details where 2 | 3 | import Prelude 4 | 5 | import Lumi.Components.Border (borderBottom, borderTop) 6 | import Lumi.Components.Column (column_) 7 | import Lumi.Components.Details (defaults, details) 8 | import Lumi.Components.Divider (divider_) 9 | import Lumi.Components.Example (example) 10 | import Lumi.Components.Icon as Icon 11 | import Lumi.Components.Row (row) 12 | import Lumi.Components.Spacing (Space(..), hspace, vspace) 13 | import Lumi.Components.Text (body_, h2_, subsectionHeader_) 14 | import React.Basic.Classic (JSX) 15 | import React.Basic.DOM as R 16 | 17 | docs :: JSX 18 | docs = 19 | column_ 20 | [ h2_ "Bare/defaults" 21 | , example $ 22 | details defaults 23 | { expanded = body_ "Here's all the info" 24 | } 25 | 26 | , h2_ "Custom arrow" 27 | , example $ 28 | details defaults 29 | { summary = iconSummary $ subsectionHeader_ "Details" 30 | , expanded = body_ "Here's all the info" 31 | } 32 | 33 | , h2_ "Accordion" 34 | , example $ 35 | borderTop $ borderBottom $ 36 | details defaults 37 | { summary = iconSummary $ subsectionHeader_ "Details" 38 | , expanded = 39 | column_ 40 | [ vspace S8 41 | , divider_ 42 | , vspace S8 43 | , body_ "Here's all the info" 44 | ] 45 | } 46 | ] 47 | where 48 | iconSummary child = 49 | row 50 | { style: R.css { alignItems: "center" } 51 | , children: [ Icon.icon_ Icon.ArrowRight, hspace S8, child ] 52 | } 53 | -------------------------------------------------------------------------------- /docs/Examples/Divider.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Divider where 2 | 3 | import Prelude 4 | 5 | import Lumi.Components.Column (column, column_) 6 | import Lumi.Components.Divider (divider_, flexDivider_) 7 | import Lumi.Components.Example (example) 8 | import Lumi.Components.Row (row) 9 | import Lumi.Components.Text as T 10 | import React.Basic.Classic (Component, JSX, createComponent, makeStateless) 11 | import React.Basic.DOM as R 12 | 13 | component :: Component Unit 14 | component = createComponent "DividerExample" 15 | 16 | docs :: JSX 17 | docs = unit # makeStateless component render 18 | where 19 | item t = 20 | T.text T.body 21 | { style = R.css { margin: "12px" } 22 | , children = [ R.text t ] 23 | } 24 | 25 | render _ = 26 | column_ 27 | [ T.text T.p 28 | { children = 29 | [ R.code_ [ R.text "hr" ] 30 | , R.text " based divider:" 31 | ] 32 | } 33 | , example $ 34 | column 35 | { style: R.css 36 | { minWidth: 300 37 | , alignItems: "center" 38 | } 39 | , children: 40 | [ item "Lorem" 41 | , divider_ 42 | , item "ipsum" 43 | ] 44 | } 45 | 46 | , T.p_ "Flexbox based divider:" 47 | , example $ 48 | row 49 | { style: R.css 50 | { minHeight: 200 51 | , alignItems: "center" 52 | } 53 | , children: 54 | [ item "Lorem" 55 | , flexDivider_ 56 | , column_ 57 | [ item "ipsum" 58 | , flexDivider_ 59 | , item "dolor" 60 | ] 61 | ] 62 | } 63 | ] 64 | -------------------------------------------------------------------------------- /docs/Examples/FixedPrecisionInput.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.FixedPrecisionInput where 2 | 3 | import Prelude 4 | 5 | import Data.Either (Either(..)) 6 | import Data.Fixed (Fixed, P100) 7 | import Effect.Uncurried (mkEffectFn2) 8 | import Lumi.Components.Spacing (vspace, Space(S12)) 9 | import Lumi.Components.Column (column_) 10 | import Lumi.Components.Text (h3_, subtext_) 11 | import Lumi.Components.FixedPrecisionInput (defaults, fixedPrecisionInput) 12 | import Lumi.Components.Example (example) 13 | import React.Basic.Classic (Component, JSX, createComponent, make) 14 | 15 | component :: Component Unit 16 | component = createComponent "FixedPrecisionInputExample" 17 | 18 | docs :: JSX 19 | docs = unit # make component 20 | { initialState 21 | , render 22 | } 23 | where 24 | initialState = 25 | { example1: Left "" :: Either String (Fixed P100) 26 | , example2: Left "" :: Either String (Fixed P100) 27 | } 28 | 29 | render self = 30 | column_ 31 | [ h3_ "Fixed P100" 32 | , example $ 33 | fixedPrecisionInput defaults 34 | { onChange = mkEffectFn2 \value _ -> 35 | self.setState _ { example1 = value } 36 | , value = self.state.example1 37 | } 38 | , subtext_ (show self.state.example1) 39 | 40 | , vspace S12 41 | , h3_ "Fixed P100 (forced invalid)" 42 | , example $ 43 | fixedPrecisionInput defaults 44 | { onChange = mkEffectFn2 \value _ -> 45 | self.setState _ { example2 = value } 46 | , value = self.state.example2 47 | } 48 | , subtext_ (show self.state.example2) 49 | ] 50 | -------------------------------------------------------------------------------- /docs/Examples/Icon.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Icon where 2 | 3 | import Prelude 4 | 5 | import Lumi.Components.Column (column_) 6 | import Lumi.Components.Icon (IconType(..), icon_) 7 | import Lumi.Components.Text (body, body_, text) 8 | import Lumi.Components.Example (example) 9 | import React.Basic.Classic (JSX) 10 | 11 | docs :: JSX 12 | docs = 13 | column_ 14 | [ text body 15 | { children = 16 | [ body_ "Arrow Down" 17 | , example $ icon_ ArrowDown 18 | , body_ "Arrow Left" 19 | , example $ icon_ ArrowLeft 20 | , body_ "Arrow Right" 21 | , example $ icon_ ArrowRight 22 | , body_ "Arrow Up" 23 | , example $ icon_ ArrowUp 24 | , body_ "Bin" 25 | , example $ icon_ Bin 26 | , body_ "Cart" 27 | , example $ icon_ Cart 28 | , body_ "Check" 29 | , example $ icon_ Check 30 | , body_ "Close" 31 | , example $ icon_ Close 32 | , body_ "Currency" 33 | , example $ icon_ Currency 34 | , body_ "Grid" 35 | , example $ icon_ Grid 36 | , body_ "Hamburger" 37 | , example $ icon_ Hamburger 38 | , body_ "Indeterminate" 39 | , example $ icon_ Indeterminate 40 | , body_ "Info" 41 | , example $ icon_ Info 42 | , body_ "List" 43 | , example $ icon_ List 44 | , body_ "Minus" 45 | , example $ icon_ Minus 46 | , body_ "Overflow" 47 | , example $ icon_ Overflow 48 | , body_ "Percent" 49 | , example $ icon_ Percent 50 | , body_ "Plus" 51 | , example $ icon_ Plus 52 | , body_ "Rearrange" 53 | , example $ icon_ Rearrange 54 | , body_ "Remove" 55 | , example $ icon_ Remove 56 | , body_ "Search" 57 | , example $ icon_ Search 58 | ] 59 | } 60 | ] 61 | -------------------------------------------------------------------------------- /docs/Examples/Images.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Images where 2 | 3 | import Prelude 4 | 5 | import Lumi.Components.Column (column_) 6 | import Lumi.Components.Images (avatar_, productThumb_) 7 | import Lumi.Components.Size (extraLarge, large, medium, small) 8 | import Lumi.Components.Text (h2_) 9 | import Lumi.Components.Example (example) 10 | import React.Basic.Classic (JSX) 11 | import React.Basic.DOM as R 12 | 13 | docs :: JSX 14 | docs = 15 | column_ 16 | [ h2_ "Avatar" 17 | 18 | , example $ 19 | avatar_ { image: R.img { src: avatarImgSrc }, size: small } 20 | , example $ 21 | avatar_ { image: R.img { src: avatarImgSrc }, size: medium } 22 | , example $ 23 | avatar_ { image: R.img { src: avatarImgSrc }, size: large } 24 | , example $ 25 | avatar_ { image: R.img { src: avatarImgSrc }, size: extraLarge } 26 | 27 | , h2_ "Product Thumb" 28 | 29 | , example $ 30 | productThumb_ { image: R.img { src: productImgSrc }, size: small } 31 | , example $ 32 | productThumb_ { image: R.img { src: productImgSrc }, size: small } 33 | , example $ 34 | productThumb_ { image: R.img { src: productImgSrc }, size: medium } 35 | , example $ 36 | productThumb_ { image: R.img { src: productImgSrc }, size: large } 37 | , example $ 38 | productThumb_ { image: R.img { src: productImgSrc }, size: extraLarge } 39 | ] 40 | where 41 | avatarImgSrc = "https://s3.amazonaws.com/lumi-blog/avatars/_x600/flexo.jpg" 42 | productImgSrc = "https://s3.amazonaws.com/lumi-flapjack-staging/mockups/9e7f08b801e6bb3a428ef72e93c49fe5.jpg" 43 | -------------------------------------------------------------------------------- /docs/Examples/Input.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Input where 2 | 3 | import Prelude 4 | import Data.Maybe (Maybe(Just)) 5 | import Data.Nullable (toNullable) 6 | import Lumi.Components.Column (column_) 7 | import Lumi.Components.Example (example) 8 | import Lumi.Components.Input (CheckboxState(..), alignToInput, checkbox, input, radio, range, switch, text_) 9 | import Lumi.Components.Row (row_) 10 | import Lumi.Components.Size (Size(..)) 11 | import Lumi.Components.Text (body, h2_, h4_, p_, text) 12 | import React.Basic.Classic (JSX) 13 | import React.Basic.DOM as R 14 | 15 | docs :: JSX 16 | docs = 17 | column_ 18 | [ h4_ "Simple text input" 19 | , example 20 | $ input text_ { placeholder = "Placeholder..." } 21 | , h4_ "Text input with a placeholder" 22 | , example 23 | $ input 24 | text_ 25 | { placeholder = "Placeholder..." 26 | , value = "This input contains a value" 27 | } 28 | , h4_ "Invalid input value" 29 | , example 30 | $ input 31 | text_ 32 | { placeholder = "Placeholder..." 33 | , pattern = toNullable $ Just "d+" 34 | , value = "An invalid value" 35 | } 36 | , h4_ "Disabled input" 37 | , example 38 | $ input 39 | text_ 40 | { placeholder = "Placeholder..." 41 | , disabled = true 42 | } 43 | , h4_ "Disabled input with a value" 44 | , example 45 | $ input 46 | text_ 47 | { placeholder = "Placeholder..." 48 | , value = "Disabled with a value" 49 | , disabled = true 50 | } 51 | , h4_ "Aligning components with input text" 52 | , p_ "`AlignToInput` applies the padding values inputs use to any child component. Use this to align elements with inputs instead of manually specifying padding." 53 | , example 54 | $ row_ 55 | [ alignToInput 56 | $ text 57 | body 58 | { children = [ R.text "Some aligned text" ] 59 | , style = R.css { whiteSpace: "nowrap" } 60 | } 61 | , input 62 | text_ 63 | { placeholder = "Placeholder..." 64 | , value = "Input value" 65 | } 66 | ] 67 | , h2_ "Checkbox" 68 | , h4_ "Medium" 69 | , exampleRow $ input checkbox { checked = Off } 70 | , exampleRow $ input checkbox { checked = On } 71 | , exampleRow $ input checkbox { checked = Indeterminate } 72 | , h4_ "Small" 73 | , exampleRow 74 | $ input 75 | checkbox 76 | { size = Small 77 | , checked = Off 78 | } 79 | , exampleRow 80 | $ input 81 | checkbox 82 | { size = Small 83 | , checked = On 84 | } 85 | , exampleRow 86 | $ input 87 | checkbox 88 | { size = Small 89 | , checked = Indeterminate 90 | } 91 | , h2_ "Radio" 92 | , h4_ "Medium" 93 | , exampleRow $ input radio { checked = Off } 94 | , exampleRow $ input radio { checked = On } 95 | , h4_ "Small" 96 | , exampleRow 97 | $ input 98 | radio 99 | { size = Small 100 | , checked = Off 101 | } 102 | , exampleRow 103 | $ input 104 | radio 105 | { size = Small 106 | , checked = On 107 | } 108 | , h2_ "Switch" 109 | , exampleRow $ input switch { checked = Off } 110 | , exampleRow $ input switch { checked = On } 111 | , h2_ "Switch (Large)" 112 | , exampleRow $ input switch { checked = Off, size = Large } 113 | , exampleRow $ input switch { checked = On, size = Large } 114 | , h2_ "Range" 115 | , exampleRow $ input range 116 | ] 117 | where 118 | exampleRow child = example $ row_ [ child ] 119 | -------------------------------------------------------------------------------- /docs/Examples/LabeledField.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.LabeledField where 2 | 3 | import Prelude 4 | 5 | import Data.Foldable (intercalate) 6 | import Data.Maybe (Maybe(..)) 7 | import Lumi.Components.Column (column, column_) 8 | import Lumi.Components.Divider (divider) 9 | import Lumi.Components.Input as Input 10 | import Lumi.Components.LabeledField (RequiredField(..), ValidationMessage(..), defaults, labeledField) 11 | import Lumi.Components.Text (h2_) 12 | import Lumi.Components.Example (example) 13 | import React.Basic.Classic (JSX) 14 | import React.Basic.DOM (css) 15 | import React.Basic.DOM as R 16 | 17 | docs :: JSX 18 | docs = 19 | column_ 20 | [ h2_ "Standard Form" 21 | , example 22 | $ column 23 | { style: css { alignSelf: "stretch" } 24 | , children: [ intercalate simpleDivider labeledFieldExamples ] 25 | } 26 | 27 | , h2_ "Forced Top Labels Form" 28 | , example 29 | $ column 30 | { style: css { alignSelf: "stretch" } 31 | , children: [ intercalate simpleDivider labeledFieldTopLabelExamples ] 32 | } 33 | 34 | , h2_ "Inline Table Form" 35 | , example 36 | $ column 37 | { style: css { alignSelf: "stretch" } 38 | , children: 39 | [ inlineTableDivider 40 | , intercalate inlineTableDivider labeledFieldExamples 41 | , inlineTableDivider 42 | ] 43 | } 44 | 45 | , h2_ "Inline Table Form (Nested)" 46 | , example 47 | $ column 48 | { style: css { alignSelf: "stretch" } 49 | , children: 50 | [ inlineTableDivider 51 | , intercalate inlineTableDivider inlineTableNestedExample 52 | , inlineTableDivider 53 | ] 54 | } 55 | ] 56 | where 57 | 58 | simpleDivider = divider { style: css { margin: "6px 0", visibility: "hidden" } } 59 | 60 | inlineTableDivider = divider { style: css { margin: "6px 0" } } 61 | 62 | makeLabeledFieldExamples forceTopLabel = 63 | [ labeledField defaults 64 | { label = R.text "First Name" 65 | , forceTopLabel = forceTopLabel 66 | , value = Input.input Input.text_ { value = "Asdf" } 67 | } 68 | , labeledField defaults 69 | { label = R.text "Last Name" 70 | , validationError = Just $ Error "Example validation error." 71 | , forceTopLabel = forceTopLabel 72 | } 73 | , labeledField defaults 74 | { label = R.text "Username" 75 | , forceTopLabel = forceTopLabel 76 | , required = Optional 77 | } 78 | , labeledField defaults 79 | { label = R.text "Password" 80 | , forceTopLabel = forceTopLabel 81 | , required = Required 82 | } 83 | , labeledField defaults 84 | { label = R.text "Admin?" 85 | , value = Input.input Input.switch 86 | , forceTopLabel = forceTopLabel 87 | } 88 | ] 89 | 90 | labeledFieldExamples = makeLabeledFieldExamples false 91 | 92 | labeledFieldTopLabelExamples = makeLabeledFieldExamples true 93 | 94 | inlineTableNestedExample = 95 | [ labeledField defaults 96 | { label = R.text "User Info" 97 | , value = intercalate inlineTableDivider labeledFieldExamples 98 | } 99 | , labeledField defaults 100 | { label = R.text "How many?" 101 | , value = Input.input Input.range 102 | } 103 | ] 104 | -------------------------------------------------------------------------------- /docs/Examples/Link.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Link where 2 | 3 | import Prelude 4 | import Data.Maybe (Maybe(..)) 5 | import Effect.Console (log) 6 | import Effect.Uncurried (mkEffectFn1) 7 | import Lumi.Components.Column (column_) 8 | import Lumi.Components.Link (link, defaults) 9 | import Lumi.Components.Text (body_) 10 | import Lumi.Components.Example (example) 11 | import React.Basic.Classic (JSX) 12 | import React.Basic.DOM as R 13 | import Web.HTML.History (URL(..)) 14 | 15 | docs :: JSX 16 | docs = 17 | column_ 18 | [ example 19 | $ R.div 20 | { onClick: mkEffectFn1 \_ -> log "Propagated" 21 | , style: R.css { display: "flex", flexDirection: "column" } 22 | , children: 23 | [ link 24 | defaults 25 | { href = URL "#/link" 26 | , navigate = pure $ log "link clicked" 27 | , text = body_ "Click here" 28 | } 29 | , link 30 | defaults 31 | { href = URL "#/input" 32 | , target = Just "_blank" 33 | , text = body_ "This should open in a new tab" 34 | } 35 | ] 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /docs/Examples/Loader.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Loader where 2 | 3 | import Prelude 4 | 5 | import Data.Nullable (null) 6 | import Lumi.Components.Column (column_) 7 | import Lumi.Components.Loader (loader) 8 | import Lumi.Components.Example (example) 9 | import React.Basic.Classic (JSX) 10 | import React.Basic.DOM as R 11 | 12 | docs :: JSX 13 | docs = 14 | column_ 15 | [ example $ 16 | loader { style: R.css {}, testId: null } 17 | ] 18 | -------------------------------------------------------------------------------- /docs/Examples/Lockup.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Lockup where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Lumi.Components.Column (column_) 7 | import Lumi.Components.Images (productThumb_) 8 | import Lumi.Components.Lockup (pageLockup, productLockup, userLockup) 9 | import Lumi.Components.Size (Size(..)) 10 | import Lumi.Components.Text (h4_) 11 | import Lumi.Components.Example (example) 12 | import React.Basic.Classic (JSX) 13 | import React.Basic.DOM as R 14 | 15 | docs :: JSX 16 | docs = 17 | column_ 18 | [ h4_ "Product Lockup" 19 | , example $ 20 | productLockup 21 | { name: "Item title" 22 | , description: Just "Foo Bar" 23 | , image: R.img { src: productImgSrc } 24 | } 25 | 26 | , h4_ "User Lockup" 27 | , example $ 28 | userLockup 29 | { name: "Flexo Rodriguez" 30 | , description: Just "Lumi" 31 | , image: R.img { src: avatarImgSrc } 32 | } 33 | 34 | , h4_ "User Lockup (no image)" 35 | , example $ 36 | userLockup 37 | { name: "Flexo Rodriguez" 38 | , description: Just "Lumi" 39 | , image: mempty 40 | } 41 | 42 | , h4_ "Page title Lockup" 43 | , example $ 44 | pageLockup 45 | { title: "Small Pouch - Rebrand" 46 | , subtitle: Just "7\" × 11\"" 47 | , image: Just $ productThumb_ { size: Medium, image: R.img { src: productImgSrc } } 48 | } 49 | ] 50 | where 51 | avatarImgSrc = "https://s3.amazonaws.com/lumi-flapjack-staging/avatars/vfhpnqairr/thumbnail_1517878176349.png" 52 | productImgSrc = "https://s3.amazonaws.com/lumi-flapjack-staging/mockups/9e7f08b801e6bb3a428ef72e93c49fe5.jpg" 53 | -------------------------------------------------------------------------------- /docs/Examples/NativeSelect.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.NativeSelect where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (fromMaybe) 6 | import Lumi.Components.Column (column_) 7 | import Lumi.Components.Example (example) 8 | import Lumi.Components.NativeSelect (defaults, nativeSelect) 9 | import React.Basic.Classic (Component, JSX, createComponent, make) 10 | import React.Basic.DOM as R 11 | import React.Basic.DOM.Events (targetValue) 12 | import React.Basic.Events (handler) 13 | 14 | component :: Component Unit 15 | component = createComponent "SelectExample" 16 | 17 | docs :: JSX 18 | docs = unit # make component 19 | { initialState 20 | , render 21 | } 22 | where 23 | opts = 24 | [ { label: "Select your car...", value: "" } 25 | , { label: "Volvo", value: "volvo" } 26 | , { label: "Saab", value: "saab" } 27 | , { label: "Mercedes", value: "mercedes" } 28 | , { label: "Audi", value: "audi" } 29 | ] 30 | 31 | initialState = { selectedOption: "" } 32 | 33 | render self = 34 | column_ 35 | [ example 36 | $ nativeSelect defaults 37 | { options = opts 38 | , onChange = handler targetValue \value -> 39 | self.setState _ { selectedOption = fromMaybe "" value } 40 | , value = self.state.selectedOption 41 | } 42 | 43 | , example 44 | $ nativeSelect defaults 45 | { options = opts 46 | , onChange = handler targetValue \value -> 47 | self.setState _ { selectedOption = fromMaybe "" value } 48 | , value = self.state.selectedOption 49 | , style = R.css { textAlignLast: "right" } 50 | } 51 | ] 52 | -------------------------------------------------------------------------------- /docs/Examples/Navigation.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Navigation where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Data.NonEmpty ((:|)) 7 | import Effect.Console (log) 8 | import Lumi.Components.Column (column_) 9 | import Lumi.Components.Example (example) 10 | import Lumi.Components.Navigation (navigation) 11 | import Lumi.Components.Text (h2_) 12 | import React.Basic.Classic (JSX) 13 | import Web.HTML.History (URL(..)) 14 | 15 | docs :: JSX 16 | docs = 17 | column_ 18 | [ h2_ "Compact navigation" 19 | , example $ 20 | navigation 21 | { navLinks: 22 | { text: "Home" 23 | , href: URL "/" 24 | , subNavLinks: [] 25 | } :| 26 | [ { text: "Customers" 27 | , href: URL "/customers" 28 | , subNavLinks: [] 29 | } 30 | , { text: "Products" 31 | , href: URL "/products" 32 | , subNavLinks: 33 | [ { text: "Products" 34 | , href: URL "/products" 35 | } 36 | , { text: "Categories" 37 | , href: URL "/categories" 38 | } 39 | ] 40 | } 41 | , { text: "Orders" 42 | , href: URL "/orders" 43 | , subNavLinks: [] 44 | } 45 | , { text: "Help" 46 | , href: URL "/help" 47 | , subNavLinks: [] 48 | } 49 | ] 50 | , user: 51 | { src: "https://s3.amazonaws.com/lumi-flapjack-staging/avatars/vfhpnqairr/thumbnail_1517878176349.png" 52 | , name: Just "Flexo" 53 | , email: Just "flexo@lumi.com" 54 | , id: "flexo-1234" 55 | } 56 | , client: 57 | { name: Nothing 58 | } 59 | , compact: true 60 | , onCartClick: Nothing 61 | , cartAmount: Nothing 62 | , onLogoutClick: Just $ log "logout click" 63 | , logo: mempty 64 | } 65 | 66 | , h2_ "Responsive navigation" 67 | , example $ 68 | navigation 69 | { navLinks: 70 | { text: "Items" 71 | , href: URL "/items" 72 | , subNavLinks: [] 73 | } :| 74 | [ { text: "Orders" 75 | , href: URL "/orders" 76 | , subNavLinks: [] 77 | } 78 | , { text: "Shipments" 79 | , href: URL "/shipments" 80 | , subNavLinks: [] 81 | } 82 | , { text: "Billing" 83 | , href: URL "/billing" 84 | , subNavLinks: [] 85 | } 86 | , { text: "Shop" 87 | , href: URL "/shop" 88 | , subNavLinks: [] 89 | } 90 | ] 91 | , user: 92 | { src: "https://s3.amazonaws.com/lumi-flapjack-staging/avatars/vfhpnqairr/thumbnail_1517878176349.png" 93 | , name: Just $ "Flexo" 94 | , email: Just $ "flexo@lumi.com" 95 | , id: "flexo-1234" 96 | } 97 | , client: 98 | { name: Just "Lumi" 99 | } 100 | , compact: false 101 | , onCartClick: Just $ log "cart click" 102 | , cartAmount: Just 2 103 | , onLogoutClick: Just $ log "logout click" 104 | , logo: mempty 105 | } 106 | ] 107 | -------------------------------------------------------------------------------- /docs/Examples/Pagination.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Pagination where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Lumi.Components.Column (column_) 7 | import Lumi.Components.Pagination (defaults, pagination) 8 | import Lumi.Components.Example (example) 9 | import React.Basic.Classic (JSX, createComponent, make) 10 | import React.Basic.DOM as R 11 | 12 | docs :: JSX 13 | docs = unit # make (createComponent "PaginationExample") 14 | { initialState 15 | , render 16 | } 17 | where 18 | initialState = 19 | { currentPage: 0 20 | } 21 | 22 | render self = 23 | column_ $ 24 | [ example 25 | $ pagination defaults 26 | { currentPage = self.state.currentPage 27 | , pages = 53 28 | , onChange = self.setState <<< flip _{ currentPage = _ } 29 | , details = Just $ R.text "531 results" 30 | } 31 | 32 | , example 33 | $ pagination defaults 34 | { currentPage = self.state.currentPage 35 | , pages = 4 36 | , onChange = self.setState <<< flip _{ currentPage = _ } 37 | , details = Just $ R.text "40 results" 38 | } 39 | 40 | , example 41 | $ pagination defaults 42 | { currentPage = self.state.currentPage 43 | , pages = 0 44 | , onChange = self.setState <<< flip _{ currentPage = _ } 45 | , details = Just $ R.text "0 results" 46 | } 47 | 48 | , example 49 | $ pagination defaults 50 | { currentPage = self.state.currentPage 51 | , pages = 40 52 | , onChange = self.setState <<< flip _{ currentPage = _ } 53 | , focusWindow = 4 54 | , marginPages = 3 55 | , details = Just $ R.text "408 results" 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /docs/Examples/Pill.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Pill where 2 | 3 | import Prelude 4 | 5 | import Lumi.Components.Column (column_) 6 | import Lumi.Components.Pill (pill_) 7 | import Lumi.Components.Example (example) 8 | import React.Basic.Classic (JSX) 9 | import Lumi.Components.Status (Status(..)) 10 | 11 | docs :: JSX 12 | docs = 13 | column_ 14 | [ example $ 15 | pill_ Active "Active" 16 | 17 | , example $ 18 | pill_ Warning "Warning" 19 | 20 | , example $ 21 | pill_ Error "Error" 22 | 23 | , example $ 24 | pill_ Unknown "Unknown" 25 | ] 26 | -------------------------------------------------------------------------------- /docs/Examples/Progress.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Progress where 2 | 3 | import Prelude 4 | 5 | import Data.Int (fromString) 6 | import Data.Maybe (Maybe(..), fromMaybe) 7 | import Data.Nullable (toNullable) 8 | import Lumi.Components.Button as Button 9 | import Lumi.Components.Column (column, column_) 10 | import Lumi.Components.Input as Input 11 | import Lumi.Components.LabeledField (labeledField, RequiredField(..)) 12 | import Lumi.Components.Progress (progressBar, progressCircle, progressDefaults) 13 | import Lumi.Components.Row (row_) 14 | import Lumi.Components.Size (Size(..)) 15 | import Lumi.Components.Text (h2_) 16 | import Lumi.Components.Example (example) 17 | import React.Basic.Classic (Component, JSX, createComponent, make) 18 | import React.Basic.DOM as R 19 | import React.Basic.DOM.Events (targetValue) 20 | import React.Basic.Events (handler, handler_) 21 | 22 | component :: Component Unit 23 | component = createComponent "ProgressExample" 24 | 25 | data Action 26 | = Set Int 27 | | Increment 28 | | Reset 29 | 30 | type State = 31 | { percent :: Int 32 | } 33 | 34 | docs :: JSX 35 | docs = unit # make component 36 | { initialState: { percent: 20 } 37 | , render: \self -> 38 | column_ 39 | [ column 40 | { style: R.css { maxWidth: "500px", padding: "20px 0" } 41 | , children: 42 | [ labeledField 43 | { label: R.text "Adjust progress:" 44 | , value: 45 | row_ 46 | [ Input.input Input.range 47 | { value = show self.state.percent 48 | , min = toNullable $ Just 0.0 49 | , max = toNullable $ Just 100.0 50 | , onChange = handler targetValue (send self <<< Set <<< fromMaybe 0 <<< flip bind fromString) 51 | , style = R.css { maxWidth: "200px" } 52 | } 53 | , Button.button Button.primary 54 | { title = "+10%" 55 | , onPress = handler_ $ send self Increment 56 | , size = Small 57 | , style = R.css { marginLeft: "16px" } 58 | } 59 | , Button.button Button.primary 60 | { title = "Reset" 61 | , onPress = handler_ $ send self Reset 62 | , size = Small 63 | , style = R.css { marginLeft: "8px" } 64 | } 65 | ] 66 | , validationError: Nothing 67 | , required: Neither 68 | , forceTopLabel: false 69 | , style: R.css {} 70 | } 71 | ] 72 | } 73 | 74 | , h2_ "Bar" 75 | , example 76 | $ progressBar progressDefaults { completed = self.state.percent } 77 | 78 | , h2_ "Circle" 79 | , example 80 | $ progressCircle progressDefaults { completed = self.state.percent } 81 | ] 82 | } 83 | where 84 | send self = case _ of 85 | Set percent -> 86 | self.setState _ { percent = percent } 87 | Increment -> 88 | self.setState \state -> state { percent = min 100 $ state.percent + 10 } 89 | Reset -> 90 | self.setState _ { percent = 0 } 91 | 92 | -------------------------------------------------------------------------------- /docs/Examples/Responsive.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Responsive where 2 | 3 | import Prelude 4 | 5 | import Lumi.Components.Column (column_) 6 | import Lumi.Components.Example (example) 7 | import Lumi.Components.Responsive (desktop, mobile, phone, withMobile) 8 | import Lumi.Components.Text (body_, p_) 9 | import React.Basic.Classic (JSX) 10 | 11 | docs :: JSX 12 | docs = 13 | column_ 14 | $ [ p_ "The following component renders different values at each of these sizes: desktop, mobile, and phone" 15 | , p_ "Note that phone sized screens still report themselves as \"mobile\" as well." 16 | , example 17 | $ column_ 18 | [ withMobile \isMobile -> 19 | body_ 20 | if isMobile then 21 | "Mobile: this text renders differently on mobile-sized screens." 22 | else 23 | "Not mobile: this text renders differently on mobile-sized screens." 24 | , mobile \_ -> 25 | body_ "Mobile: this text is only rendered on mobile-sized screens." 26 | , desktop \_ -> 27 | body_ "Desktop: this text is only rendered on desktop-sized screens." 28 | , phone \_ -> 29 | body_ "Phone: this text is only rendered on phone-sized screens." 30 | ] 31 | ] 32 | -------------------------------------------------------------------------------- /docs/Examples/Row.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Row where 2 | 3 | import Prelude 4 | 5 | import Lumi.Components.Button (button, primary) 6 | import Lumi.Components.Column (column_) 7 | import Lumi.Components.Row (responsiveRow_, row_) 8 | import Lumi.Components.Text (h2_, p_) 9 | import Lumi.Components.Example (example) 10 | import React.Basic.Classic (JSX) 11 | 12 | docs :: JSX 13 | docs = 14 | column_ 15 | [ example 16 | $ row_ 17 | [ button primary { title = "Button" } 18 | , button primary { title = "Button" } 19 | , button primary { title = "Button" } 20 | ] 21 | 22 | , h2_ "Responsive row" 23 | , p_ "* Resize the window to see how the component responds." 24 | , example 25 | $ responsiveRow_ 26 | [ button primary { title = "Button" } 27 | , button primary { title = "Button" } 28 | , button primary { title = "Button" } 29 | ] 30 | ] 31 | -------------------------------------------------------------------------------- /docs/Examples/ScrollManager.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.ScrollManager where 2 | 3 | import Lumi.Components.Column (column_) 4 | import Lumi.Components.Text (p_) 5 | import Lumi.Components.Example (exampleCode) 6 | import React.Basic.Classic (JSX) 7 | 8 | docs :: JSX 9 | docs = 10 | column_ 11 | [ p_ "Used to normalize forward/back button scroll behavior across browsers." 12 | , p_ "`ScrollManager` should be used at the root of the application, just below the `react-router` `Router` component, as it only supports Window scrolling. For example:" 13 | , exampleCode """ReactDOM.render( 14 | 15 | 16 | 17 | 18 | , 19 | document.getElementById("root") 20 | ); 21 | """ 22 | ] 23 | -------------------------------------------------------------------------------- /docs/Examples/Slider.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Slider where 2 | 3 | import Prelude 4 | 5 | import Effect.Uncurried (mkEffectFn1) 6 | import Lumi.Components.Column (column_) 7 | import Lumi.Components.Text (p_) 8 | import Lumi.Components.Slider (slider) 9 | import Lumi.Components.Example (example) 10 | import React.Basic.Classic (Component, JSX, createComponent, make) 11 | import React.Basic.DOM as R 12 | 13 | component :: Component Unit 14 | component = createComponent "SliderExample" 15 | 16 | docs :: JSX 17 | docs = unit # make component 18 | { initialState 19 | , render 20 | } 21 | where 22 | initialState = 23 | { percent: 50 24 | } 25 | 26 | render self = 27 | column_ 28 | [ p_ $ show self.state.percent <> "%" 29 | , example $ 30 | slider 31 | { completed: self.state.percent 32 | , onChange: mkEffectFn1 \percent -> self.setState _ { percent = percent } 33 | , style: R.css { } 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /docs/Examples/Spacing.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Spacing where 2 | 3 | import Prelude 4 | 5 | import Color (cssStringHSLA) 6 | import Lumi.Components.Color (colors) 7 | import Lumi.Components.Column (column_) 8 | import Lumi.Components.Example (example) 9 | import Lumi.Components.Row (row) 10 | import Lumi.Components.Spacing (Space(..), hspace, vspace) 11 | import Lumi.Components.Text (body_) 12 | import React.Basic.Classic (JSX) 13 | import React.Basic.DOM as R 14 | 15 | docs :: JSX 16 | docs = 17 | column_ 18 | [ spacingExample "4px" S4 19 | , spacingExample "8px" S8 20 | , spacingExample "12px" S12 21 | , spacingExample "16px" S16 22 | , spacingExample "24px" S24 23 | , spacingExample "32px" S32 24 | , spacingExample "48px" S48 25 | , spacingExample "56px" S56 26 | , spacingExample "64px" S64 27 | , spacingExample "72px" S72 28 | , spacingExample "80px" S80 29 | , spacingExample "88px" S88 30 | , spacingExample "96px" S96 31 | , spacingExample "104px" S104 32 | , spacingExample "112px" S112 33 | ] 34 | where 35 | spacingExample text size = 36 | example $ 37 | row 38 | { style: R.css { alignItems: "center" } 39 | , children: 40 | [ body_ text 41 | , hspace S12 42 | , row 43 | { style: R.css { backgroundColor: cssStringHSLA colors.black3 } 44 | , children: 45 | [ R.div_ [ vspace S64 ] 46 | , hspace size 47 | ] 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /docs/Examples/StatusSlat.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.StatusSlat where 2 | 3 | import Prelude 4 | 5 | import Data.Nullable (notNull, null) 6 | import Lumi.Components.Column (column_) 7 | import Lumi.Components.Example (example) 8 | import Lumi.Components.Status (Status(..)) 9 | import Lumi.Components.StatusSlat (statusSlat) 10 | import React.Basic.Classic (JSX) 11 | 12 | docs :: JSX 13 | docs = 14 | column_ 15 | [ example $ 16 | statusSlat { data: statusSlat_one } 17 | , example $ 18 | statusSlat { data: statusSlat_two } 19 | , example $ 20 | statusSlat { data: statusSlat_three } 21 | , example $ 22 | statusSlat { data: statusSlat_four } 23 | ] 24 | where 25 | statusSlat_one = 26 | [ { label: "Status" 27 | , content: "Complete" 28 | , status: notNull Active 29 | } 30 | , { label: "Subtext" 31 | , content: "Product name" 32 | , status: null 33 | } 34 | , { label: "Date ordered" 35 | , content: "2018-02-13" 36 | , status: null 37 | } 38 | , { label: "Total" 39 | , content: "$20.000" 40 | , status: null 41 | } 42 | ] 43 | 44 | statusSlat_two = 45 | [ { label: "Status" 46 | , content: "Complete" 47 | , status: notNull Active 48 | } 49 | , { label: "Payment status" 50 | , content: "Paid" 51 | , status: null 52 | } 53 | ] 54 | 55 | statusSlat_three = 56 | [ { label: "Status" 57 | , content: "Cancelled" 58 | , status: notNull Error 59 | } 60 | , { label: "Price" 61 | , content: "$0.01" 62 | , status: null 63 | } 64 | , { label: "Due date" 65 | , content: "2/14/18" 66 | , status: notNull Unknown 67 | } 68 | ] 69 | 70 | statusSlat_four = 71 | [ { label: "Status" 72 | , content: "In progress" 73 | , status: notNull Warning 74 | } 75 | , { label: "Weight" 76 | , content: "5.00 lb" 77 | , status: null 78 | } 79 | , { label: "Size" 80 | , content: "Medium" 81 | , status: null 82 | } 83 | , { label: "Price" 84 | , content: "$6.54" 85 | , status: null 86 | } 87 | ] 88 | -------------------------------------------------------------------------------- /docs/Examples/Svg.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Svg where 2 | 3 | import Prelude 4 | 5 | import Lumi.Components.Column (column_) 6 | import Lumi.Components.Example (example) 7 | import Lumi.Components.Svg (userSvg, clientSvg) 8 | import Lumi.Components.Text (h2_) 9 | import React.Basic.Classic (JSX) 10 | import React.Basic.DOM as R 11 | 12 | docs :: JSX 13 | docs = 14 | column_ 15 | [ h2_ "Client" 16 | , svgExample clientSvg 17 | 18 | , h2_ "User" 19 | , svgExample userSvg 20 | ] 21 | where 22 | svgExample svg = 23 | example $ R.div 24 | { style: R.css { maxHeight: 200, maxWidth: 200, height: "100%", width: "100%" } 25 | , children: [ svg ] 26 | } 27 | 28 | -------------------------------------------------------------------------------- /docs/Examples/Tab.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Tab where 2 | 3 | import Prelude 4 | 5 | import Control.Alternative (guard) 6 | import Data.Array (index, (..)) 7 | import Data.Maybe (Maybe(..), fromMaybe) 8 | import Data.String (Pattern(..), split, toLower) 9 | import Effect.Uncurried (runEffectFn1) 10 | import Lumi.Components.Column (column_) 11 | import Lumi.Components.Tab (TabId(..), TabKey(..), tabs, urlParts) 12 | import Lumi.Components.Text (h2_) 13 | import Lumi.Components.Utility.ReactRouter (RouterProps, withRouter) 14 | import Lumi.Components.Example (example) 15 | import React.Basic.Classic (Component, JSX, createComponent, element, empty, toReactComponent) 16 | import React.Basic.DOM as R 17 | import Web.HTML.History (URL(..)) 18 | 19 | component :: Component (RouterProps ()) 20 | component = createComponent "TabExample" 21 | 22 | docs :: JSX 23 | docs = flip element {} $ withRouter $ toReactComponent identity component { render: \{ props } -> render props } 24 | where 25 | render props = 26 | column_ 27 | [ h2_ "Demo 1" 28 | , example $ 29 | R.div 30 | { style: R.css { maxWidth: "800px" } 31 | , children: 32 | [ tabs 33 | { currentLocation: URL $ "#" <> props.location.pathname <> props.location.search <> props.location.hash 34 | , useHash: true 35 | , navigate: Just \url -> 36 | let 37 | parts = urlParts url 38 | newUrl = parts.path <> parts.query <> parts.hash.path <> parts.hash.query 39 | newUrlNoHash = fromMaybe "" $ flip index 1 $ split (Pattern "#") newUrl 40 | in 41 | runEffectFn1 props.history.push $ URL $ newUrlNoHash 42 | , queryKey: TabKey "demo1" 43 | , style: R.css {} 44 | , tabStyle: R.css {} 45 | , selectedTabStyle: R.css {} 46 | , tabs: 1 .. 10 <#> \i -> 47 | let 48 | label = "Tab with a long title " <> show i 49 | in 50 | { content: \_ -> empty 51 | , id: TabId (toLower label) 52 | , label 53 | , count: (7*(i - 1) * (i - 1) `mod` 4) * 7 <$ guard (i `mod` 3 /= 1) 54 | , testId: Nothing 55 | } 56 | } 57 | ] 58 | } 59 | 60 | , h2_ "Demo 2" 61 | , example $ 62 | tabs 63 | { currentLocation: URL $ "#" <> props.location.pathname <> props.location.search <> props.location.hash 64 | , useHash: true 65 | , navigate: Just \url -> 66 | let 67 | parts = urlParts url 68 | newUrl = parts.path <> parts.query <> parts.hash.path <> parts.hash.query 69 | newUrlNoHash = fromMaybe "" $ flip index 1 $ split (Pattern "#") newUrl 70 | in 71 | runEffectFn1 props.history.push $ URL $ newUrlNoHash 72 | , queryKey: TabKey "demo2" 73 | , style: R.css {} 74 | , tabStyle: R.css {} 75 | , selectedTabStyle: R.css {} 76 | , tabs: 1 .. 6 <#> \i -> 77 | let 78 | label = "Tab" <> show i 79 | in 80 | { content: \_ -> empty 81 | , id: TabId (toLower label) 82 | , label 83 | , count: (i `div` 2) <$ guard (i `mod` 4 == 1) 84 | , testId: Nothing 85 | } 86 | } 87 | ] 88 | -------------------------------------------------------------------------------- /docs/Examples/Text.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Text where 2 | 3 | import Prelude 4 | 5 | import Lumi.Components.Column (column_) 6 | import Lumi.Components.Text (body_, h1_, h2_, h3_, h4_, h5_, h6_, mainHeader_, p_, sectionHeader_, span_, subsectionHeader_, subtext_, title_, paragraph_) 7 | import Lumi.Components.Example (example) 8 | import React.Basic.Classic (JSX) 9 | 10 | docs :: JSX 11 | docs = 12 | column_ 13 | [ p_ "There are six text sizes, each displayed below with its name. The lumi tags have no default padding. These sizes have also been mapped onto the browser tags with default padding." 14 | , example 15 | $ column_ 16 | [ mainHeader_ "MainHeader" 17 | , title_ "Title" 18 | , sectionHeader_ "SectionHeader" 19 | , subsectionHeader_ "SubsectionHeader" 20 | , body_ "Body" 21 | , paragraph_ "Paragraph" 22 | , subtext_ "Subtext" 23 | 24 | , h1_ "h1" 25 | , h2_ "h2" 26 | , h3_ "h3" 27 | , h4_ "h4" 28 | , h5_ "h5" 29 | , h6_ "h6" 30 | , p_ "p" 31 | , span_ "span" 32 | ] 33 | ] 34 | -------------------------------------------------------------------------------- /docs/Examples/Textarea.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Textarea where 2 | 3 | import Prelude 4 | 5 | import Data.Foldable (traverse_) 6 | import Lumi.Components.Column (column, column_) 7 | import Lumi.Components.Text (p_) 8 | import Lumi.Components.Textarea (defaults, textarea) 9 | import Lumi.Components.Example (example) 10 | import React.Basic.Classic (Component, JSX, createComponent, make) 11 | import React.Basic.DOM (css) 12 | import React.Basic.DOM.Events (targetValue) 13 | import React.Basic.Events (handler) 14 | 15 | component :: Component Unit 16 | component = createComponent "TextareaExample" 17 | 18 | docs :: JSX 19 | docs = unit # make component { initialState , render } 20 | where 21 | initialState = 22 | { example1: "" 23 | } 24 | 25 | render self = 26 | column_ 27 | [ p_ "Empty" 28 | , example $ 29 | column 30 | { style: css { alignSelf: "stretch" } 31 | , children: 32 | [ textarea defaults 33 | { lines = 3 34 | , placeholder = "Enter text here..." 35 | , value = self.state.example1 36 | , onChange = handler targetValue $ traverse_ \value -> self.setState _ { example1 = value } 37 | } 38 | ] 39 | } 40 | ] 41 | -------------------------------------------------------------------------------- /docs/Examples/Toast.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Toast where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..), fromMaybe) 6 | import Effect.Random (random) 7 | import Lumi.Components.Button (button, primary) 8 | import Lumi.Components.ButtonGroup (buttonGroup) 9 | import Lumi.Components.Column (column_) 10 | import Lumi.Components.Input (input, text_) 11 | import Lumi.Components.Text (p, text) 12 | import Lumi.Components.Toast (toast, toastBubble, toastManager) 13 | import Lumi.Components.Example (example) 14 | import React.Basic.Classic (Component, JSX, createComponent, keyed, make) 15 | import React.Basic.DOM as R 16 | import React.Basic.DOM.Events (targetValue) 17 | import React.Basic.Events (handler, handler_) 18 | 19 | component :: Component Unit 20 | component = createComponent "ToastExample" 21 | 22 | docs :: JSX 23 | docs = unit # make component { initialState, render } 24 | where 25 | initialState = 26 | { message: Nothing 27 | , pendingMessage: "This is an example message" 28 | , key: "" 29 | } 30 | 31 | render self = 32 | column_ 33 | [ example $ 34 | toastBubble { message: self.state.pendingMessage } 35 | 36 | , text p 37 | { children = 38 | [ R.text "Click here to trigger a toast notification: " 39 | , buttonGroup 40 | { children: 41 | [ input text_ 42 | { value = self.state.pendingMessage 43 | , onChange = handler targetValue \targetValue -> do 44 | self.setState _ { pendingMessage = fromMaybe "" targetValue } 45 | } 46 | , button primary 47 | { onPress = handler_ do 48 | key <- show <$> random 49 | self.setState _ { key = key, message = Just self.state.pendingMessage } 50 | , title = "Notify" 51 | } 52 | ] 53 | , joined: true 54 | , style: R.css {} 55 | } 56 | , toastManager 57 | , keyed self.state.key $ 58 | toast { message: self.state.message } 59 | ] 60 | } 61 | ] 62 | -------------------------------------------------------------------------------- /docs/Examples/Tooltip.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Examples.Tooltip where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Data.Nullable (toNullable) 7 | import Lumi.Components.Size (Size(..)) 8 | import Lumi.Components.Column (column_) 9 | import Lumi.Components.Link as Link 10 | import Lumi.Components.Row (row_) 11 | import Lumi.Components.Text (body_) 12 | import Lumi.Components.Text as T 13 | import Lumi.Components.Example (example) 14 | import Lumi.Components.Tooltip (tooltip) 15 | import React.Basic.Classic (JSX) 16 | import React.Basic.DOM as R 17 | import Web.HTML.History (URL(..)) 18 | 19 | docs :: JSX 20 | docs = 21 | column_ 22 | [ example 23 | $ tooltip 24 | { variant: "basic" 25 | , style: R.css {} 26 | , text: R.text "Lorem ipsum" 27 | , content: body_ "Basic example" 28 | , size: toNullable Nothing 29 | } 30 | 31 | , example 32 | $ tooltip 33 | { variant: "top" 34 | , style: R.css {} 35 | , text: R.text "Lorem ipsum" 36 | , content: body_ "Top example" 37 | , size: toNullable Nothing 38 | } 39 | 40 | , example 41 | $ tooltip 42 | { variant: "bottom" 43 | , style: R.css {} 44 | , text: R.text "Lorem ipsum" 45 | , content: body_ "Bottom example" 46 | , size: toNullable Nothing 47 | } 48 | 49 | , example 50 | $ tooltip 51 | { variant: "left" 52 | , style: R.css {} 53 | , text: R.text "Lorem ipsum" 54 | , content: body_ "Left example" 55 | , size: toNullable Nothing 56 | } 57 | 58 | , example 59 | $ row_ 60 | [ body_ "Hello, world see" 61 | , tooltip 62 | { variant: "top" 63 | , style: R.css { padding: "0 2px", textDecoration: "underline" } 64 | , text: R.text "Lorem ipsum" 65 | , content: body_ "here" 66 | , size: toNullable Nothing 67 | } 68 | , body_ "for more." 69 | ] 70 | 71 | , example 72 | $ row_ 73 | [ body_ "Hello, world see" 74 | , tooltip 75 | { variant: "top" 76 | , style: R.css { padding: "0 2px", textDecoration: "underline" } 77 | , text: R.text "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis auctor libero non libero consequat, at iaculis diam venenatis. Donec nec porttitor tellus." 78 | , content: body_ "here" 79 | , size: toNullable $ Just $ Large 80 | } 81 | , body_ "for more." 82 | ] 83 | 84 | , example 85 | $ row_ 86 | [ body_ "Hello, world see" 87 | , tooltip 88 | { variant: "top" 89 | , style: R.css { padding: "0 2px", textDecoration: "underline" } 90 | , text: 91 | T.text T.body 92 | { style = R.css {} 93 | , children = 94 | [ R.text "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis auctor libero non libero consequat, at iaculis diam venenatis. Donec nec porttitor tellus " 95 | , Link.link Link.defaults 96 | { href = URL "#/link" 97 | , text = R.text "click here" 98 | , target = Just "_blank" 99 | } 100 | , R.text "." 101 | ] 102 | } 103 | , content: body_ "here" 104 | , size: toNullable $ Just $ Large 105 | } 106 | , body_ "for more (includes link in tooltip)." 107 | ] 108 | ] 109 | -------------------------------------------------------------------------------- /docs/Examples2/Box.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components2.Examples.Box where 2 | 3 | import Prelude 4 | 5 | import Lumi.Components (($$$)) 6 | import Lumi.Components.Example (example) 7 | import Lumi.Components.MUIColorScheme (blue, green, red) 8 | import Lumi.Components.Spacing (Space(..), vspace) 9 | import Lumi.Components.Text (h2_, p_) 10 | import Lumi.Components2.Box (box, column, row) 11 | import Lumi.Styles.Box (FlexAlign(..), _alignSelf, _justify, _row) 12 | import React.Basic.Classic (JSX) 13 | import React.Basic.Emotion as E 14 | 15 | docs :: JSX 16 | docs = 17 | let 18 | childBox color = 19 | box _ 20 | { css = 21 | \_ -> 22 | E.css 23 | { backgroundColor: E.color color 24 | , height: E.prop S40 25 | , width: E.prop S40 26 | } 27 | } 28 | 29 | exampleContent = 30 | [ childBox red 31 | , childBox green 32 | , childBox blue 33 | ] 34 | in 35 | box 36 | _ 37 | { content = 38 | [ p_ "Box is a simple building block component. Lumi components default to building off of Box and generally expect their JSX arguments to be Box-compatible elements." 39 | , p_ "A Box is essentially a div which defaults to a flex column. The most common flex settings are available as prop modifiers. Nested boxes will also stretch to fill their given space by default. If a component shouldn't grow beyond a specific size under any circumstances, be sure to give it a max-width!" 40 | , vspace S24 41 | , h2_ "Defaults" 42 | , example 43 | $ box 44 | $$$ exampleContent 45 | , h2_ "Row" 46 | , example 47 | $ box 48 | $ _row 49 | $$$ exampleContent 50 | , h2_ "Align/justify" 51 | , example 52 | $ row 53 | $ _alignSelf Stretch -- only necessary because `example` isn't a Box 54 | $ _justify End 55 | $$$ exampleContent 56 | , h2_ "Space evenly" 57 | , example 58 | $ column 59 | $ _alignSelf Stretch -- only necessary because `example` isn't a Box 60 | $ _justify SpaceEvenly 61 | $$$ exampleContent 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /docs/Examples2/ButtonGroup.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components2.Examples.ButtonGroup where 2 | 3 | import Prelude 4 | 5 | import Effect.Console (log) 6 | import Effect.Uncurried (mkEffectFn1) 7 | import Lumi.Components (($$$)) 8 | import Lumi.Components.Column (column_) 9 | import Lumi.Components.Example (example) 10 | import Lumi.Components.NativeSelect (nativeSelect, defaults) 11 | import Lumi.Components.Text (h2_) 12 | import Lumi.Components2.Button (button, secondary) 13 | import Lumi.Components2.ButtonGroup (buttonGroup, joined) 14 | import React.Basic.Classic (JSX) 15 | import React.Basic.DOM as R 16 | 17 | docs :: JSX 18 | docs = 19 | column_ 20 | $ [ h2_ "Not Joined" 21 | , example 22 | $ buttonGroup 23 | $$$ 24 | [ button $$$ [ R.text "Button" ] 25 | , button 26 | $ secondary 27 | $$$ [ R.text "Button" ] 28 | ] 29 | , h2_ "Not Joined" 30 | , example 31 | $ buttonGroup 32 | $$$ 33 | [ button $$$ [ R.text "Button" ] 34 | , button 35 | $ secondary 36 | $$$ [ R.text "Button" ] 37 | , button 38 | $ secondary 39 | $$$ [ R.text "Button" ] 40 | ] 41 | , h2_ "Not Joined" 42 | , example 43 | $ buttonGroup 44 | $$$ 45 | [ button $$$ [ R.text "Button" ] 46 | , nativeSelect 47 | defaults 48 | { options = [] 49 | , onChange = mkEffectFn1 \_ -> log "onChange" 50 | , value = "Foo bar" 51 | } 52 | , button 53 | $ secondary 54 | $$$ [ R.text "Button" ] 55 | ] 56 | , h2_ "Joined" 57 | , example 58 | $ buttonGroup 59 | $ joined 60 | $$$ 61 | [ button 62 | $ secondary 63 | $$$ [ R.text "Button" ] 64 | , button 65 | $ secondary 66 | $$$ [ R.text "Button" ] 67 | ] 68 | , h2_ "Joined" 69 | , example 70 | $ buttonGroup 71 | $ joined 72 | $$$ 73 | [ button 74 | $ secondary 75 | $$$ [ R.text "Button" ] 76 | , button 77 | $ secondary 78 | $$$ [ R.text "Button" ] 79 | , button 80 | $ secondary 81 | $$$ [ R.text "Button" ] 82 | ] 83 | ] 84 | -------------------------------------------------------------------------------- /docs/Examples2/Clip.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components2.Examples.Clip where 2 | 3 | import Prelude 4 | 5 | import Lumi.Components (($$$)) 6 | import Lumi.Components.Example (example) 7 | import Lumi.Components.Spacing (Space(..), vspace) 8 | import Lumi.Components2.Text as T 9 | import Lumi.Components2.Box (box) 10 | import Lumi.Components2.Clip (clip) 11 | import Lumi.Styles as S 12 | import React.Basic.Classic (JSX) 13 | 14 | docs :: JSX 15 | docs = 16 | box 17 | _ 18 | { content = 19 | [ T.paragraph_ $$$ "The Clip component wraps the provided content with a grey border and a \"Copy\" button, which copies the text content into the system clipboard." 20 | , T.paragraph_ $$$ "If clipboard access is not allowed or not supported the text will be left highlighted, allowing the user to press ctrl+c manually. Only the plain text content is copied, not the HTML." 21 | , vspace S24 22 | , example 23 | $ clip 24 | $ _ { content = 25 | [ T.paragraph 26 | $ S.style_ (S.css { marginBottom: S.px 0 }) 27 | $ T.truncate 28 | $$$ [ T.text $$$ "someone@email.com" ] 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /docs/Examples2/Image.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components2.Examples.Image where 2 | 3 | import Prelude 4 | 5 | import Data.Array as Array 6 | import Lumi.Components (($$$)) 7 | import Lumi.Components.Example (example) 8 | import Lumi.Components.Spacing (Space(..), vspace) 9 | import Lumi.Components.Text (h4_) 10 | import Lumi.Components2.Image as Image 11 | import React.Basic.Classic (JSX) 12 | 13 | docs :: JSX 14 | docs = 15 | let flexo = "https://i.picsum.photos/id/985/1000/300.jpg?hmac=t_lmj43iuzwGqZaRMY1ee9udE_pCzfYLgCD49YrCPjw" 16 | -- "https://s3.amazonaws.com/lumi-blog/avatars/_x600/flexo.jpg" 17 | in Array.intercalate (vspace S16) 18 | [ h4_ "Image default (will respect image's original aspect ratio & dimensions)" 19 | , example 20 | $ Image.image 21 | $$$ flexo 22 | , h4_ "Image + resize { width: 40px, height: 120px }, the image will fill the height and width of its parent (object-fit: cover), maintaining its aspect ratio but cropping the image" 23 | , example 24 | $ Image.image 25 | $ Image.resize { width: 40, height: 120 } 26 | $$$ flexo 27 | , h4_ "Thumbnail default (will always have a square aspect ratio); and defaults to object-fit: cover" 28 | , example 29 | $ Image.thumbnail 30 | $$$ flexo 31 | , h4_ "Thumbnail default (will always have a square aspect ratio); but can be override with object-fit: contain" 32 | , example 33 | $ Image.thumbnail 34 | $ Image.contain 35 | $ _ { content = flexo 36 | } 37 | , h4_ "Thumbnail + resize 400px" 38 | , example 39 | $ Image.thumbnail 40 | $ Image.resizeSquare 400 41 | $$$ flexo 42 | , h4_ "Thumbnail + small" 43 | , example 44 | $ Image.thumbnail 45 | $ Image.small 46 | $$$ flexo 47 | , h4_ "Thumbnail + medium" 48 | , example 49 | $ Image.thumbnail 50 | $ Image.medium 51 | $$$ flexo 52 | , h4_ "Thumbnail + large" 53 | , example 54 | $ Image.thumbnail 55 | $ Image.large 56 | $$$ flexo 57 | , h4_ "Thumbnail + extra large" 58 | , example 59 | $ Image.thumbnail 60 | $ Image.extraLarge 61 | $$$ flexo 62 | , h4_ "Thumbnail + avatar (small)" 63 | , example 64 | $ Image.thumbnail 65 | $ Image.smallAvatar 66 | $$$ flexo 67 | , h4_ "Thumbnail + avatar (medium)" 68 | , example 69 | $ Image.thumbnail 70 | $ Image.mediumAvatar 71 | $$$ flexo 72 | , h4_ "Thumbnail + avatar (large)" 73 | , example 74 | $ Image.thumbnail 75 | $ Image.largeAvatar 76 | $$$ flexo 77 | , h4_ "Default placeholder (can be overriden)" 78 | , example 79 | $ Image.image 80 | $ Image.resize { width: 320, height: 240 } 81 | $$$ "" 82 | , example 83 | $ Image.thumbnail 84 | $ Image.medium 85 | $$$ "" 86 | ] 87 | -------------------------------------------------------------------------------- /docs/Examples2/Link.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components2.Examples.Link where 2 | 3 | import Prelude 4 | import Data.Maybe (Maybe(..)) 5 | import Effect.Console (log) 6 | import Effect.Uncurried (mkEffectFn1) 7 | import Lumi.Components.Column (column_) 8 | import Lumi.Components.Example (example) 9 | import Lumi.Components.Text (body_) 10 | import Lumi.Components2.Link (link) 11 | import React.Basic.Classic (JSX) 12 | import React.Basic.DOM as R 13 | import Web.HTML.History (URL(..)) 14 | 15 | docs :: JSX 16 | docs = 17 | column_ 18 | [ example 19 | $ R.div 20 | { onClick: mkEffectFn1 \_ -> log "Propagated" 21 | , style: R.css { display: "flex", flexDirection: "column" } 22 | , children: 23 | [ link 24 | _ 25 | { href = URL "#/link" 26 | , navigate = pure $ log "link clicked" 27 | , content = [ body_ "Click here" ] 28 | } 29 | , link 30 | _ 31 | { href = URL "#/input" 32 | , target = Just "_blank" 33 | , content = [ body_ "This should open in a new tab" ] 34 | } 35 | ] 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /docs/Examples2/QRCode.example.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components2.Examples.QRCode where 2 | 3 | import Prelude 4 | import Data.Foldable (traverse_) 5 | import Data.Maybe (Maybe(..), fromMaybe) 6 | import Effect.Unsafe (unsafePerformEffect) 7 | import Lumi.Components.Example (example) 8 | import Lumi.Components.Input as Input 9 | import Lumi.Components.Spacing (Space(..), vspace) 10 | import Lumi.Components.Text (subsectionHeader_) 11 | import Lumi.Components2.Box (box) 12 | import Lumi.Components2.Link (link) 13 | import Lumi.Components2.QRCode (ErrorCorrectLevel(..), useQRCode) 14 | import Lumi.Styles as S 15 | import Lumi.Styles.Border as Border 16 | import Lumi.Styles.Box (FlexAlign(..)) 17 | import Lumi.Styles.Box as Box 18 | import React.Basic.DOM.Events (capture, targetValue) 19 | import React.Basic.Hooks (JSX, component, useState, (/\)) 20 | import React.Basic.Hooks as React 21 | import Web.HTML.History (URL(..)) 22 | 23 | docs :: JSX 24 | docs = 25 | unit # unsafePerformEffect do 26 | component "QRCodeExample" \_ -> React.do 27 | value /\ setValue <- useState "https://www.lumi.com" 28 | pure 29 | $ box 30 | _ 31 | { content = 32 | [ Input.input 33 | Input.text_ 34 | { value = value 35 | , onChange = capture targetValue $ traverse_ (const >>> setValue) 36 | } 37 | , vspace S24 38 | , example 39 | $ qrcodeExample { value } 40 | ] 41 | } 42 | 43 | qrcodeExample :: { value :: String } -> JSX 44 | qrcodeExample = 45 | unsafePerformEffect do 46 | component "QRCode" \props -> React.do 47 | { qrcode, url } <- useQRCode ECLLow props.value 48 | pure 49 | $ box 50 | $ Box._align Center 51 | $ _ 52 | { content = 53 | [ qrcode 54 | $ Border.border 55 | $ Border._round 56 | $ S.style_ 57 | ( S.css 58 | { padding: S.px 16 59 | , width: S.px 140 60 | } 61 | ) 62 | $ identity 63 | , vspace S8 64 | , link 65 | _ 66 | { href = fromMaybe (URL "") url 67 | , download = Just "qrcode.svg" 68 | , content = 69 | [ subsectionHeader_ "Download SVG" 70 | ] 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /docs/MUIColorScheme.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.MUIColorScheme where 2 | 3 | -- Copied from the deleted material UI color scheme from 4 | -- from https://github.com/purescript-contrib/purescript-colors/commit/0ec954c1c248ae601ee6c9eee0b60aa30f717241 5 | 6 | -- | Material Design color scheme, see 7 | -- | https://www.google.com/design/spec/style/color.html 8 | import Color (Color, rgb) 9 | 10 | red :: Color 11 | red = rgb 244 67 54 12 | 13 | pink :: Color 14 | pink = rgb 233 30 99 15 | 16 | purple :: Color 17 | purple = rgb 156 39 176 18 | 19 | deepPurple :: Color 20 | deepPurple = rgb 103 58 183 21 | 22 | indigo :: Color 23 | indigo = rgb 63 81 181 24 | 25 | blue :: Color 26 | blue = rgb 33 150 243 27 | 28 | lightBlue :: Color 29 | lightBlue = rgb 3 169 244 30 | 31 | cyan :: Color 32 | cyan = rgb 0 188 212 33 | 34 | teal :: Color 35 | teal = rgb 0 150 136 36 | 37 | green :: Color 38 | green = rgb 76 175 80 39 | 40 | lightGreen :: Color 41 | lightGreen = rgb 139 195 74 42 | 43 | lime :: Color 44 | lime = rgb 205 220 57 45 | 46 | yellow :: Color 47 | yellow = rgb 255 235 59 48 | 49 | amber :: Color 50 | amber = rgb 255 193 7 51 | 52 | orange :: Color 53 | orange = rgb 255 152 0 54 | 55 | deepOrange :: Color 56 | deepOrange = rgb 255 87 34 57 | 58 | brown :: Color 59 | brown = rgb 121 85 72 60 | 61 | grey :: Color 62 | grey = rgb 158 158 158 63 | 64 | blueGrey :: Color 65 | blueGrey = rgb 96 125 139 -------------------------------------------------------------------------------- /docs/ScrollManager.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { withRouter } from "react-router"; 3 | 4 | // Delay a callback `fn` until the returned function 5 | // has not been called for `ms` milliseconds. 6 | const debounceFn = (ms, fn) => { 7 | let timeout = null; 8 | return () => { 9 | clearTimeout(timeout); 10 | timeout = setTimeout(fn, ms); 11 | }; 12 | }; 13 | 14 | // Retry an operation every `ms` milliseconds until 15 | // `fn` returns true or `maxTries` is reached. 16 | const retryFn = (ms, maxTries, fn) => { 17 | if (fn()) return; 18 | 19 | const innerRetryFn = currentTry => { 20 | if (currentTry > maxTries) return; 21 | setTimeout(() => { 22 | if (!fn()) { 23 | innerRetryFn(currentTry + 1); 24 | } 25 | }, ms); 26 | }; 27 | innerRetryFn(2); 28 | }; 29 | 30 | const restoreScroll = props => { 31 | const { history } = props; 32 | switch (history.action) { 33 | case "PUSH": 34 | // New forward navigation -- scroll to top 35 | window.scrollTo(0, 0); 36 | break; 37 | case "POP": 38 | // Back or forward navigation -- attempt scroll restore if necessary 39 | retryFn(props.onScrollDebounceMs, props.scrollRestoreMaxRetries, () => { 40 | // Short circuit if there's no scroll position to restore to 41 | if (!window.history.state) return true; 42 | 43 | const { scrollX, scrollY } = window.history.state; 44 | if (scrollX !== window.scrollX || scrollY !== window.scrollY) { 45 | // Scroll position out of sync -- attempt reset and loop 46 | // (if a render is unusually slow the scroll attempt may fail) 47 | window.scrollTo(scrollX, scrollY); 48 | return false; 49 | } else { 50 | // Scroll position restored 51 | return true; 52 | } 53 | }); 54 | break; 55 | } 56 | }; 57 | 58 | const ScrollManager = props => { 59 | useEffect(() => { 60 | // Save the scroll position any time scrolling stops 61 | const scrollListener = debounceFn(props.onScrollDebounceMs, () => { 62 | const { history, scrollX, scrollY } = window; 63 | if ( 64 | !history.state || 65 | scrollX !== history.state.scrollX || 66 | scrollY !== history.state.scrollY 67 | ) { 68 | history.replaceState({ scrollX, scrollY }, "", history.location); 69 | } 70 | }); 71 | window.addEventListener("scroll", scrollListener); 72 | restoreScroll(props); 73 | return () => { 74 | window.removeEventListener("scroll", scrollListener); 75 | }; 76 | }, [props.onScrollDebounceMs, props.scrollRestoreMaxRetries]); 77 | 78 | return props.children; 79 | }; 80 | 81 | ScrollManager.defaultProps = { 82 | onScrollDebounceMs: 50, 83 | scrollRestoreMaxRetries: 40 84 | }; 85 | 86 | export default withRouter(ScrollManager); 87 | -------------------------------------------------------------------------------- /docs/Utility/ReactRouter.js: -------------------------------------------------------------------------------- 1 | export { withRouter } from "react-router"; 2 | -------------------------------------------------------------------------------- /docs/Utility/ReactRouter.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Utility.ReactRouter where 2 | 3 | import Prelude 4 | 5 | import Effect.Uncurried (EffectFn1) 6 | import React.Basic.Classic (ReactComponent) 7 | import Web.HTML.History (URL) 8 | 9 | type RouterProps props = 10 | { history :: { push :: EffectFn1 URL Unit } 11 | , location :: { pathname :: String, search :: String, hash :: String } 12 | | props 13 | } 14 | 15 | foreign import withRouter 16 | :: forall props 17 | . ReactComponent (RouterProps props) 18 | -> ReactComponent { | props } 19 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | Lumi Components 11 | 12 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { HashRouter } from "react-router-dom"; 4 | import ScrollManager from "./ScrollManager"; 5 | import App from "./App"; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | 11 | 12 | , 13 | document.getElementById("root") 14 | ); 15 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1652776076, 6 | "narHash": "sha256-gzTw/v1vj4dOVbpBSJX4J0DwUR6LIyXo7/SuuTJp1kM=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "04c1b180862888302ddfb2e3ad9eaa63afc60cf8", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nixpkgs": { 19 | "locked": { 20 | "lastModified": 1653326962, 21 | "narHash": "sha256-W8feCYqKTsMre4nAEpv5Kx1PVFC+hao/LwqtB2Wci/8=", 22 | "owner": "NixOS", 23 | "repo": "nixpkgs", 24 | "rev": "41cc1d5d9584103be4108c1815c350e07c807036", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "NixOS", 29 | "ref": "nixpkgs-unstable", 30 | "repo": "nixpkgs", 31 | "type": "github" 32 | } 33 | }, 34 | "root": { 35 | "inputs": { 36 | "flake-utils": "flake-utils", 37 | "nixpkgs": "nixpkgs" 38 | } 39 | } 40 | }, 41 | "root": "root", 42 | "version": 7 43 | } 44 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Provide an environment for working in this repo"; 3 | 4 | # to handle mac and linux 5 | inputs.flake-utils.url = "github:numtide/flake-utils"; 6 | 7 | # we want to use a consistent nixpkgs across developers. 8 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 9 | 10 | outputs = all@{ self, nixpkgs, flake-utils, ... }: 11 | flake-utils.lib.eachDefaultSystem (system: 12 | let 13 | pkgs = import nixpkgs { 14 | inherit system; 15 | }; 16 | packages = 17 | let 18 | # everything we want available in our development environment that isn't managed by 19 | # npm, spago 20 | # we do not differentiate between libraries needed for building and tools at the moment. 21 | sharedPackages = with pkgs; [ 22 | nodejs-16_x 23 | ]; 24 | in 25 | sharedPackages; 26 | in { 27 | # produce our actual shell 28 | devShell = pkgs.mkShell rec { 29 | # make our packages available 30 | buildInputs = packages; 31 | }; 32 | } 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-lumi-components", 3 | "version": "0.0.0", 4 | "description": "Lumi UI components", 5 | "private": true, 6 | "main": "index.js", 7 | "homepage": "https://lumihq.github.io/purescript-lumi-components", 8 | "repository": "lumihq/purescript-lumi-components", 9 | "files": [ 10 | "src/components" 11 | ], 12 | "scripts": { 13 | "start": "run-s watch", 14 | "prewatch": "run-s eslint build:purs", 15 | "watch": "run-p watch:js watch:purs", 16 | "watch:js": "webpack-dev-server -wd --port 3000 --host 0.0.0.0", 17 | "watch:purs": "pulp -w build -I docs", 18 | "prebuild": "run-s eslint", 19 | "build": "run-s build:purs build:js", 20 | "build:js": "webpack -p", 21 | "build:purs": "pulp build -I docs", 22 | "setup": "npm install && bower install && spago install", 23 | "eslint": "eslint --ext .js,.jsx src docs webpack.config.js", 24 | "pscid": "pscid -I docs", 25 | "pscid:build": "run-s build:purs", 26 | "predeploy": "run-s build", 27 | "deploy": "gh-pages -d build", 28 | "clean": "rm -rf build output generated-docs .psa-stash .pulp-cache .psci-modules", 29 | "clean:deps": "rm -rf node_modules bower_components", 30 | "clean:everything": "run-s clean clean:deps" 31 | }, 32 | "dependencies": { 33 | "@emotion/react": "^11.9.3", 34 | "big-integer": "^1.6.48", 35 | "jss": "^10.3.0", 36 | "jss-preset-default": "^10.3.0", 37 | "qrcode.react": "^3.1.0", 38 | "react": "^18.0.0", 39 | "react-dnd": "^11.0.0", 40 | "react-dnd-html5-backend": "^11.0.0", 41 | "react-dom": "^18.0.0", 42 | "react-media-hook": "^0.5.0", 43 | "react-password-strength-bar": "^0.4.1" 44 | }, 45 | "devDependencies": { 46 | "@babel/core": "^7.10.4", 47 | "@babel/plugin-proposal-class-properties": "^7.10.4", 48 | "@babel/plugin-proposal-object-rest-spread": "^7.10.4", 49 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 50 | "@babel/preset-env": "^7.10.4", 51 | "@babel/preset-react": "^7.10.4", 52 | "babel-eslint": "^10.1.0", 53 | "babel-loader": "^8.1.0", 54 | "bower": "^1.8.4", 55 | "change-case": "^4.1.1", 56 | "chokidar": "^3.5.3", 57 | "clean-webpack-plugin": "^3.0.0", 58 | "eslint": "^7.3.1", 59 | "eslint-config-prettier": "^6.11.0", 60 | "eslint-plugin-prettier": "^3.1.4", 61 | "eslint-plugin-react": "^7.20.3", 62 | "gh-pages": "^3.1.0", 63 | "git-revision-webpack-plugin": "^3.0.6", 64 | "html-webpack-plugin": "^4.3.0", 65 | "npm-run-all": "^4.1.5", 66 | "null-loader": "^4.0.0", 67 | "purescript": "0.15.2", 68 | "prettier": "^2.0.5", 69 | "pscid": "^2.9.3", 70 | "pulp": "^16.0.0", 71 | "purescript-psa": "^0.7.3", 72 | "react-loadable": "^5.5.0", 73 | "react-media": "^1.10.0", 74 | "react-router": "^5.2.0", 75 | "react-router-dom": "^5.2.0", 76 | "resource-hints-webpack-plugin": "^0.0.2", 77 | "spago": "^0.21.0", 78 | "script-ext-html-webpack-plugin": "^2.1.4", 79 | "source-map-loader": "^1.0.1", 80 | "terser-webpack-plugin": "^3.0.6", 81 | "webpack": "^4.43.0", 82 | "webpack-bundle-analyzer": "^3.8.0", 83 | "webpack-cli": "^3.3.12", 84 | "webpack-dev-server": "^3.11.0", 85 | "webpack-manifest-plugin": "^2.2.0", 86 | "webpack-subresource-integrity": "^1.4.1" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.9-20230629/packages.dhall 3 | sha256:f91d36c7e4793fe4d7e042c57fef362ff3f9e9ba88454cd38686701e30bf545a 4 | 5 | let additions = 6 | { foreign-generic = 7 | { dependencies = 8 | [ "arrays" 9 | , "assert" 10 | , "bifunctors" 11 | , "console" 12 | , "control" 13 | , "effect" 14 | , "either" 15 | , "exceptions" 16 | , "foldable-traversable" 17 | , "foreign" 18 | , "foreign-object" 19 | , "identity" 20 | , "lists" 21 | , "maybe" 22 | , "newtype" 23 | , "partial" 24 | , "prelude" 25 | , "record" 26 | , "strings" 27 | , "transformers" 28 | , "tuples" 29 | , "typelevel-prelude" 30 | , "unsafe-coerce" 31 | ] 32 | , repo = "https://github.com/lumihq/purescript-foreign-generic.git" 33 | , version = "proxy-instances" 34 | } 35 | , promises = 36 | { dependencies = 37 | [ "console" 38 | , "datetime" 39 | , "effect" 40 | , "exceptions" 41 | , "functions" 42 | , "prelude" 43 | , "transformers" 44 | , "arrays" 45 | , "either" 46 | , "foldable-traversable" 47 | , "unfoldable" 48 | , "maybe" 49 | ] 50 | , repo = "https://github.com/clipperz/purescript-promises.git" 51 | , version = "a87a9b1cbab5446574a289a770c4f26bcd9dd21d" 52 | } 53 | } 54 | 55 | in (upstream // additions) 56 | with metadata.version = "v0.15.2" 57 | -------------------------------------------------------------------------------- /spago.dhall: -------------------------------------------------------------------------------- 1 | { name = "lumi-components" 2 | , packages = ./packages.dhall 3 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 4 | , license = "Apache-2.0" 5 | , repository = "https://github.com/lumihq/purescript-lumi-components" 6 | , dependencies = 7 | [ "aff" 8 | , "aff-coroutines" 9 | , "arrays" 10 | , "bifunctors" 11 | , "colors" 12 | , "console" 13 | , "control" 14 | , "coroutines" 15 | , "datetime" 16 | , "effect" 17 | , "either" 18 | , "enums" 19 | , "exceptions" 20 | , "fixed-precision" 21 | , "foldable-traversable" 22 | , "foreign" 23 | , "foreign-object" 24 | , "free" 25 | , "heterogeneous" 26 | , "integers" 27 | , "js-timers" 28 | , "js-uri" 29 | , "maybe" 30 | , "media-types" 31 | , "newtype" 32 | , "nonempty" 33 | , "nullable" 34 | , "numbers" 35 | , "ordered-collections" 36 | , "parallel" 37 | , "partial" 38 | , "prelude" 39 | , "profunctor-lenses" 40 | , "react-basic" 41 | , "react-basic-classic" 42 | , "react-basic-dnd" 43 | , "react-basic-dom" 44 | , "react-basic-emotion" 45 | , "react-basic-hooks" 46 | , "record" 47 | , "refs" 48 | , "simple-json" 49 | , "st" 50 | , "strings" 51 | , "tailrec" 52 | , "transformers" 53 | , "tuples" 54 | , "unsafe-coerce" 55 | , "unsafe-reference" 56 | , "web-dom" 57 | , "web-events" 58 | , "web-file" 59 | , "web-html" 60 | , "web-storage" 61 | , "web-uievents" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /src/JSS/JSS.js: -------------------------------------------------------------------------------- 1 | import * as jssGlobal from 'jss'; 2 | export {default as preset} from 'jss-preset-default'; 3 | 4 | export const createInstance_ = function (p) { 5 | return jssGlobal.create(p()); 6 | }; 7 | 8 | export const createStyleSheet_ = function (jss, styles) { 9 | return jss.createStyleSheet(styles); 10 | }; 11 | 12 | export const globalAttachStyleSheet_ = function (stylesheet) { 13 | return stylesheet.attach(); 14 | }; 15 | 16 | export const toStringStyleSheet = function (stylesheet) { 17 | return stylesheet.toString(); 18 | }; 19 | -------------------------------------------------------------------------------- /src/JSS/JSS.purs: -------------------------------------------------------------------------------- 1 | module JSS 2 | ( JSSConfig 3 | , JSSInstance 4 | , JSSStyleSheet 5 | , JSS 6 | , JSSValue 7 | , jss 8 | , createInstance 9 | , preset 10 | , createStyleSheet 11 | , globalAttachStyleSheet 12 | , toStringStyleSheet 13 | , important 14 | , jssValue 15 | ) where 16 | 17 | import Prelude 18 | 19 | import Effect (Effect) 20 | import Effect.Uncurried (EffectFn1, EffectFn2, runEffectFn1, runEffectFn2) 21 | import Unsafe.Coerce (unsafeCoerce) 22 | 23 | foreign import data JSSConfig :: Type 24 | 25 | foreign import data JSSInstance :: Type 26 | 27 | foreign import data JSSStyleSheet :: Type 28 | 29 | foreign import data JSS :: Type 30 | 31 | foreign import data JSSValue :: Type 32 | 33 | jss :: forall styles. {| styles } -> JSS 34 | jss = unsafeCoerce 35 | 36 | createInstance :: Effect JSSConfig -> Effect JSSInstance 37 | createInstance = runEffectFn1 createInstance_ 38 | 39 | foreign import createInstance_ :: EffectFn1 (Effect JSSConfig) JSSInstance 40 | 41 | foreign import preset :: Effect JSSConfig 42 | 43 | createStyleSheet :: JSSInstance -> JSS -> Effect JSSStyleSheet 44 | createStyleSheet = runEffectFn2 createStyleSheet_ 45 | 46 | foreign import createStyleSheet_ :: EffectFn2 JSSInstance JSS JSSStyleSheet 47 | 48 | globalAttachStyleSheet :: JSSStyleSheet -> Effect Unit 49 | globalAttachStyleSheet = runEffectFn1 globalAttachStyleSheet_ 50 | 51 | foreign import globalAttachStyleSheet_ :: EffectFn1 JSSStyleSheet Unit 52 | 53 | foreign import toStringStyleSheet :: JSSStyleSheet -> String 54 | 55 | -- | Allows the mixing of types in the values passed to JSS. 56 | -- | For example, to create `!important` properties JSS 57 | -- | sometimes requires heterogeneous arrays: 58 | -- | 59 | -- | ```purs 60 | -- | [ [ "10px" ], "!important" ] 61 | -- | ``` 62 | jssValue :: forall a. a -> JSSValue 63 | jssValue = unsafeCoerce 64 | 65 | important :: forall a. a -> JSSValue 66 | important a = jssValue [ jssValue [ a ], jssValue "!important" ] 67 | -------------------------------------------------------------------------------- /src/Lumi/Components/Badge.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Badge where 2 | 3 | import Prelude 4 | 5 | import Color (cssStringHSLA) 6 | import Effect.Unsafe (unsafePerformEffect) 7 | import JSS (JSS, jss) 8 | import Lumi.Components.Color (Color, colors) 9 | import Lumi.Components.Text (lumiSubtextFontSize) 10 | import React.Basic.Classic (JSX, element) 11 | import React.Basic.DOM as R 12 | 13 | type BadgeProps = 14 | { background :: Color 15 | , color :: Color 16 | , style :: R.CSS 17 | , text :: String 18 | } 19 | 20 | badge_ :: String -> JSX 21 | badge_ = badge <<< defaults { text = _ } 22 | 23 | badge :: BadgeProps -> JSX 24 | badge = \{ background, color, style, text } -> 25 | lumiBadgeElement 26 | { style: R.mergeStyles 27 | [ R.css 28 | { color: cssStringHSLA color 29 | , background: cssStringHSLA background 30 | } 31 | , style 32 | ] 33 | , children: R.text text 34 | } 35 | where 36 | lumiBadgeElement = element (unsafePerformEffect $ R.unsafeCreateDOMComponent "lumi-badge") 37 | 38 | defaults :: BadgeProps 39 | defaults = 40 | { background: colors.black5 41 | , color: colors.secondary 42 | , style: R.css {} 43 | , text: "" 44 | } 45 | 46 | styles :: JSS 47 | styles = jss 48 | { "@global": 49 | { "lumi-badge": 50 | { alignSelf: "baseline" 51 | , display: "inline" 52 | , fontSize: lumiSubtextFontSize 53 | , lineHeight: "17px" 54 | , borderRadius: "9px" 55 | , marginTop: "2px" 56 | , padding: "0px 6px" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Lumi/Components/Border.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Border where 2 | 3 | import Prelude 4 | 5 | import Color (toHexString) 6 | import Effect.Unsafe (unsafePerformEffect) 7 | import JSS (JSS, jss) 8 | import Lumi.Components.Color (colors) 9 | import React.Basic.Classic (Component, JSX, createComponent, element, makeStateless) 10 | import React.Basic.DOM (unsafeCreateDOMComponent) 11 | 12 | data BorderStyle 13 | = SquareTop 14 | | SquareBottom 15 | | Round 16 | | Square 17 | | Top 18 | | Bottom 19 | 20 | toStyle :: BorderStyle -> String 21 | toStyle SquareTop = "square-top" 22 | toStyle SquareBottom = "square-bottom" 23 | toStyle Round = "round" 24 | toStyle Square = "square" 25 | toStyle Top = "top" 26 | toStyle Bottom = "bottom" 27 | 28 | component :: Component BorderProps 29 | component = createComponent "Border" 30 | 31 | type BorderProps = 32 | { children :: JSX 33 | , borderStyle :: BorderStyle 34 | } 35 | 36 | border :: BorderProps -> JSX 37 | border = makeStateless component render 38 | where 39 | lumiBorderElement = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-border") 40 | 41 | render props = 42 | lumiBorderElement 43 | { "data-border-style": toStyle props.borderStyle 44 | , children: props.children 45 | } 46 | 47 | borderSquare :: JSX -> JSX 48 | borderSquare children = 49 | border 50 | { children 51 | , borderStyle: Square 52 | } 53 | 54 | borderRound :: JSX -> JSX 55 | borderRound children = 56 | border 57 | { children 58 | , borderStyle: Round 59 | } 60 | 61 | borderSquareTop :: JSX -> JSX 62 | borderSquareTop children = 63 | border 64 | { children 65 | , borderStyle: SquareTop 66 | } 67 | 68 | borderSquareBottom :: JSX -> JSX 69 | borderSquareBottom children = 70 | border 71 | { children 72 | , borderStyle: SquareBottom 73 | } 74 | 75 | borderTop :: JSX -> JSX 76 | borderTop children = 77 | border 78 | { children 79 | , borderStyle: Top 80 | } 81 | 82 | borderBottom :: JSX -> JSX 83 | borderBottom children = 84 | border 85 | { children 86 | , borderStyle: Bottom 87 | } 88 | 89 | 90 | styles :: JSS 91 | styles = jss 92 | { "@global": 93 | { "lumi-border": 94 | { borderColor: toHexString colors.black3 95 | , borderStyle: "solid" 96 | , borderWidth: "1px" 97 | , padding: "16px" 98 | , overflow: "hidden" 99 | , "&[data-border-style='round']": 100 | { borderRadius: "5px" 101 | } 102 | , "&[data-border-style='square']": 103 | { borderRadius: "0px" 104 | } 105 | , "&[data-border-style='square-bottom']": 106 | { borderTopRightRadius: "5px" 107 | , borderTopLeftRadius: "5px" 108 | , borderBottomRightRadius: "0px" 109 | , borderBottomLeftRadius: "0px" 110 | } 111 | , "&[data-border-style='square-top']": 112 | { borderTopRightRadius: "0px" 113 | , borderTopLeftRadius: "0px" 114 | , borderBottomRightRadius: "5px" 115 | , borderBottomLeftRadius: "5px" 116 | } 117 | , "&[data-border-style='top']": 118 | { border: "inherit" 119 | , borderTop: "1px solid " <> toHexString colors.black3 120 | , paddingLeft: "0px" 121 | , paddingRight: "0px" 122 | } 123 | , "&[data-border-style='bottom']": 124 | { border: "inherit" 125 | , borderBottom: "1px solid " <> toHexString colors.black3 126 | , paddingLeft: "0px" 127 | , paddingRight: "0px" 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Lumi/Components/ButtonGroup.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.ButtonGroup where 2 | 3 | import Prelude 4 | 5 | import Effect.Unsafe (unsafePerformEffect) 6 | import JSS (JSS, jss) 7 | import Lumi.Components.ZIndex (ziButtonGroup) 8 | import React.Basic.Classic (Component, JSX, createComponent, element, makeStateless) 9 | import React.Basic.DOM (unsafeCreateDOMComponent, CSS) 10 | 11 | type ButtonGroupProps = 12 | { children :: Array JSX 13 | , style :: CSS 14 | , joined :: Boolean 15 | } 16 | 17 | component :: Component ButtonGroupProps 18 | component = createComponent "ButtonGroup" 19 | 20 | buttonGroup :: ButtonGroupProps -> JSX 21 | buttonGroup = makeStateless component render 22 | where 23 | render props = 24 | buttonGroupElement 25 | { "data-joined": props.joined 26 | , style: props.style 27 | , children: map renderChild props.children 28 | } 29 | where 30 | renderChild child = 31 | buttonGroupChildElement { children: [ child ] } 32 | 33 | buttonGroupElement = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-button-group") 34 | buttonGroupChildElement = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-button-group-child") 35 | 36 | styles :: JSS 37 | styles = jss 38 | { "@global": 39 | { "lumi-button-group": 40 | { height: "100%" 41 | , display: "flex" 42 | , flexFlow: "row" 43 | 44 | , "&[data-joined=false] > lumi-button-group-child": 45 | { marginRight: 10 46 | , "&:last-child": { marginRight: 0 } 47 | } 48 | 49 | , "&[data-joined=true] > lumi-button-group-child": 50 | { "&:not(:last-child) button.lumi": 51 | { marginRight: -1 52 | , borderTopRightRadius: 0 53 | , borderBottomRightRadius: 0 54 | } 55 | , "&:not(:first-child) button.lumi": 56 | { borderTopLeftRadius: 0 57 | , borderBottomLeftRadius: 0 58 | } 59 | , "&:focus": { zIndex: ziButtonGroup } 60 | , "&:hover": { zIndex: ziButtonGroup } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Lumi/Components/Color.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Color 2 | ( Color 3 | , ColorName 4 | , ColorMap 5 | , colors 6 | , colorNames 7 | , shade 8 | ) where 9 | 10 | import Prelude 11 | 12 | import Color (darken, desaturate, lighten, rgb, rgba) 13 | import Color as C 14 | import Data.Newtype (class Newtype) 15 | 16 | type Color = C.Color 17 | 18 | newtype ColorName = ColorName String 19 | 20 | derive instance newtypeColorName :: Newtype ColorName _ 21 | 22 | type ColorMap a = 23 | { black :: a 24 | , black1 :: a 25 | , black2 :: a 26 | , black3 :: a 27 | , black4 :: a 28 | , black5 :: a 29 | , black6 :: a 30 | , black7 :: a 31 | , black8 :: a 32 | , primary :: a 33 | , primary1 :: a 34 | , primary2 :: a 35 | , primary3 :: a 36 | , primary4 :: a 37 | , secondary :: a 38 | , accent1 :: a 39 | , accent11 :: a 40 | , accent12 :: a 41 | , accent13 :: a 42 | , accent2 :: a 43 | , accent21 :: a 44 | , accent22 :: a 45 | , accent23 :: a 46 | , accent3 :: a 47 | , accent31 :: a 48 | , accent32 :: a 49 | , accent33 :: a 50 | , white :: a 51 | , transparent :: a 52 | } 53 | 54 | colors :: ColorMap Color 55 | colors = 56 | { black: rgb 0x23 0x22 0x20 57 | , black1: rgb 0x91 0x90 0x8d 58 | , black2: rgb 0xbe 0xbc 0xb9 59 | , black3: rgb 0xdd 0xdd 0xdc 60 | , black4: rgb 0xe7 0xe6 0xe5 61 | , black5: rgb 0xf5 0xf4 0xf2 62 | , black6: rgb 0xf7 0xf6 0xf4 63 | , black7: rgb 0xf9 0xf8 0xf7 64 | , black8: rgb 0xfb 0xfa 0xf9 65 | , primary: rgb 0 150 212 66 | , primary1: rgb 151 221 243 67 | , primary2: rgb 181 230 246 68 | , primary3: rgb 211 241 250 69 | , primary4: rgb 235 247 253 70 | , secondary: rgb 0x91 0x90 0x8d 71 | , accent1: rgb 0x49 0xb8 0x60 72 | , accent11: rgb 0x78 0xdd 0x8d 73 | , accent12: rgb 0xbc 0xee 0xc6 74 | , accent13: rgb 0xe4 0xf5 0xe7 75 | , accent2: rgb 0xff 0xa5 0x02 76 | , accent21: rgb 0xff 0xb9 0x67 77 | , accent22: rgb 0xff 0xdc 0xb3 78 | , accent23: rgb 0xff 0xee 0xd9 79 | , accent3: rgb 0xf1 0x50 0x0d 80 | , accent31: rgb 0xf7 0x96 0x6d 81 | , accent32: rgb 0xfb 0xca 0xb6 82 | , accent33: rgb 0xfd 0xe5 0xdb 83 | , white: rgb 0xff 0xff 0xff 84 | , transparent: rgba 0 0 0 0.0 85 | } 86 | 87 | colorNames :: ColorMap ColorName 88 | colorNames = 89 | { black: ColorName "black" 90 | , black1: ColorName "black-1" 91 | , black2: ColorName "black-2" 92 | , black3: ColorName "black-3" 93 | , black4: ColorName "black-4" 94 | , black5: ColorName "black-5" 95 | , black6: ColorName "black-6" 96 | , black7: ColorName "black-7" 97 | , black8: ColorName "black-8" 98 | , primary: ColorName "primary" 99 | , primary1: ColorName "primary-1" 100 | , primary2: ColorName "primary-2" 101 | , primary3: ColorName "primary-3" 102 | , primary4: ColorName "primary-4" 103 | , secondary: ColorName "secondary" 104 | , accent1: ColorName "accent-1" 105 | , accent11: ColorName "accent-1-1" 106 | , accent12: ColorName "accent-1-2" 107 | , accent13: ColorName "accent-1-3" 108 | , accent2: ColorName "accent-2" 109 | , accent21: ColorName "accent-2-1" 110 | , accent22: ColorName "accent-2-2" 111 | , accent23: ColorName "accent-2-3" 112 | , accent3: ColorName "accent-3" 113 | , accent31: ColorName "accent-3-1" 114 | , accent32: ColorName "accent-3-2" 115 | , accent33: ColorName "accent-3-3" 116 | , white: ColorName "white" 117 | , transparent: ColorName "transparent" 118 | } 119 | 120 | shade :: 121 | { hue :: Color, white :: Color, black :: Color } -> 122 | { black :: Color 123 | , grey1 :: Color 124 | , grey2 :: Color 125 | , hue :: Color 126 | , hueDarker :: Color 127 | , hueDarkest :: Color 128 | , hueDisabled :: Color 129 | , white :: Color 130 | } 131 | shade { hue, white, black } = 132 | let 133 | hueDarker = darken 0.1 hue 134 | hueDarkest = darken 0.15 hue 135 | hueDisabled = lighten 0.4137 $ desaturate 0.1972 hue 136 | grey1 = lighten 0.7 black 137 | grey2 = lighten 0.78 black 138 | in 139 | { hue, hueDarker, hueDarkest, hueDisabled, grey1, grey2, white, black } 140 | -------------------------------------------------------------------------------- /src/Lumi/Components/Column.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Column where 2 | 3 | import Prelude 4 | 5 | import Effect.Unsafe (unsafePerformEffect) 6 | import JSS (JSS, jss) 7 | import React.Basic.Classic (Component, JSX, createComponent, element, makeStateless) 8 | import React.Basic.DOM (CSS, css, unsafeCreateDOMComponent) 9 | 10 | component :: Component ColumnProps 11 | component = createComponent "Column" 12 | 13 | type ColumnProps = 14 | { children :: Array JSX 15 | , style :: CSS 16 | } 17 | 18 | column :: ColumnProps -> JSX 19 | column = makeStateless component lumiColumnElement 20 | where 21 | lumiColumnElement = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-column") 22 | 23 | column_ :: Array JSX -> JSX 24 | column_ children = column { children, style: css {} } 25 | 26 | columnSelfStretch :: Array JSX -> JSX 27 | columnSelfStretch children = column 28 | { style: css { alignSelf: "stretch" } 29 | , children 30 | } 31 | 32 | responsiveColumn :: ColumnProps -> JSX 33 | responsiveColumn = makeStateless component lumiResponsiveColumnElement 34 | where 35 | lumiResponsiveColumnElement = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-responsive-column") 36 | 37 | responsiveColumn_ :: Array JSX -> JSX 38 | responsiveColumn_ children = responsiveColumn { children, style: css {} } 39 | 40 | styles :: JSS 41 | styles = jss 42 | { "@global": 43 | { "lumi-column": 44 | { boxSizing: "border-box" 45 | , display: "flex" 46 | , flexDirection: "column" 47 | } 48 | 49 | , "lumi-responsive-column": 50 | { boxSizing: "border-box" 51 | , display: "flex" 52 | , flexDirection: "column" 53 | 54 | , "@media (max-width: 860px)": 55 | { flexDirection: "row" 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Lumi/Components/Details.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Details where 2 | 3 | import Prelude 4 | 5 | import Effect.Unsafe (unsafePerformEffect) 6 | import JSS (JSS, jss) 7 | import Lumi.Components.Text (body_) 8 | import React.Basic.DOM as R 9 | import React.Basic.Hooks (JSX, component) 10 | 11 | type DetailsProps = 12 | { summary :: JSX 13 | , expanded :: JSX 14 | , defaultOpen :: Boolean 15 | } 16 | 17 | details :: DetailsProps -> JSX 18 | details = unsafePerformEffect do 19 | component "Details" \props -> 20 | pure $ R.details 21 | { className: "lumi" 22 | , children: 23 | [ R.summary 24 | { className: "lumi" 25 | , children: [ props.summary ] 26 | } 27 | , props.expanded 28 | ] 29 | } 30 | 31 | defaults :: DetailsProps 32 | defaults = 33 | { summary: body_ "Details" 34 | , expanded: mempty 35 | , defaultOpen: false 36 | } 37 | 38 | styles :: JSS 39 | styles = jss 40 | { "@global": 41 | { "details.lumi": 42 | { "& > summary.lumi": 43 | { listStyle: "none" 44 | 45 | , "&:focus": 46 | { outline: "none" 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Lumi/Components/Divider.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Divider where 2 | 3 | import Prelude 4 | 5 | import Color (cssStringHSLA) 6 | import Effect.Unsafe (unsafePerformEffect) 7 | import JSS (JSS, jss) 8 | import Lumi.Components.Color (colors) 9 | import React.Basic.Classic (Component, JSX, createComponent, element, makeStateless) 10 | import React.Basic.DOM as R 11 | 12 | type DividerProps = 13 | { style :: R.CSS 14 | } 15 | 16 | component :: Component DividerProps 17 | component = createComponent "Divider" 18 | 19 | divider :: DividerProps -> JSX 20 | divider = makeStateless component $ R.hr <<< mapProps 21 | where 22 | mapProps props = 23 | { className: "lumi" 24 | , style: props.style 25 | } 26 | 27 | divider_ :: JSX 28 | divider_ = divider { style: R.css {} } 29 | 30 | flexDivider :: DividerProps -> JSX 31 | flexDivider = \{ style } -> lumiFlexDividerElement { style } 32 | where 33 | lumiFlexDividerElement = element (unsafePerformEffect $ R.unsafeCreateDOMComponent "lumi-flex-divider") 34 | 35 | flexDivider_ :: JSX 36 | flexDivider_ = flexDivider { style: R.css {} } 37 | 38 | styles :: JSS 39 | styles = jss 40 | { "@global": 41 | { "hr.lumi": 42 | { height: "1px" 43 | , alignSelf: "stretch" 44 | , color: cssStringHSLA colors.black4 45 | , background: cssStringHSLA colors.black4 46 | , fontSize: "0" 47 | , border: "0" 48 | , flexShrink: "0" 49 | , flexBasis: "1px" 50 | } 51 | 52 | , "lumi-flex-divider": 53 | { alignSelf: "stretch" 54 | , color: cssStringHSLA colors.black4 55 | , background: cssStringHSLA colors.black4 56 | , fontSize: "0" 57 | , border: "0" 58 | , flexShrink: "0" 59 | , flexBasis: "1px" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Lumi/Components/DropdownButton.js: -------------------------------------------------------------------------------- 1 | export const checkIsEventTargetInTree = function (domNode, e) { 2 | return e.target === domNode || domNode.contains(e.target); 3 | }; 4 | -------------------------------------------------------------------------------- /src/Lumi/Components/FixedPrecisionInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Input(props) { 4 | var this_ = this; 5 | this_.bindRef = function (input) { 6 | this_.inputRef = input; 7 | }; 8 | this_.onChange = function (e) { 9 | var target = e.target; 10 | var valueStr = target.value; 11 | var validationError = props.validate(valueStr); 12 | if (validationError) { 13 | target.setCustomValidity(validationError); 14 | return; 15 | } 16 | var value = parseFloat(valueStr); 17 | if (!isNaN(value)) { 18 | if (value > props.max) { 19 | target.setCustomValidity( 20 | "Value must be less than or equal to " + props.max + "." 21 | ); 22 | } else if (value < props.min) { 23 | target.setCustomValidity( 24 | "Value must be greater than or equal to " + props.min + "." 25 | ); 26 | } else { 27 | target.setCustomValidity(""); 28 | } 29 | } 30 | }; 31 | return this_; 32 | } 33 | 34 | Input.prototype = Object.create(React.Component.prototype); 35 | 36 | Input.displayName = "FixedPrecisionInputInner"; 37 | 38 | Input.prototype.componentWillReceiveProps = function (newProps) { 39 | if (this.inputRef != null && this.inputRef.value !== newProps.defaultValue) { 40 | this.inputRef.value = newProps.defaultValue; 41 | } 42 | }; 43 | 44 | Input.prototype.render = function () { 45 | return React.createElement( 46 | "input", 47 | Object.assign({}, this.props, { 48 | ref: this.bindRef, 49 | onChange: this.onChange, 50 | validate: undefined 51 | }) 52 | ); 53 | }; 54 | 55 | export const input_ = Input; 56 | 57 | export const cancelWhenNotMatch = function (regex, e) { 58 | if (e.cancelable && !e.defaultPrevented && !regex.test(e.key)) { 59 | e.preventDefault(); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /src/Lumi/Components/Form/Defaults.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Form.Defaults 2 | ( class FormDefaults 3 | , formDefaults 4 | , class FormDefaultsRecord 5 | , formDefaultsRecordBuilder 6 | ) where 7 | 8 | import Prelude 9 | 10 | import Data.Array.NonEmpty (NonEmptyArray) 11 | import Data.Either (Either(..)) 12 | import Data.Map (Map) 13 | import Data.Map as Map 14 | import Data.Set (Set) 15 | import Data.Set as Set 16 | import Data.Maybe (Maybe(..)) 17 | import Data.Symbol (class IsSymbol) 18 | import Lumi.Components.Form.Validation (Validated(..)) 19 | import Prim.Row (class Cons, class Lacks) 20 | import Prim.RowList (class RowToList, Cons, Nil) 21 | import Record.Builder (Builder) 22 | import Record.Builder as Builder 23 | import Type.Proxy (Proxy(..)) 24 | import Foreign.Object (Object) 25 | import Foreign.Object as Object 26 | 27 | -- | Provides default values for primitive data types to be used as initial 28 | -- | values in form builders. 29 | -- | 30 | -- | For example: 31 | -- | 32 | -- | ```purescript 33 | -- | build_ formDefaults ado 34 | -- | firstName <- focus_ (prop (Proxy :: Proxy "firstName")) textbox 35 | -- | lastName <- focus_ (prop (Proxy :: Proxy "lastName")) textbox 36 | -- | in { firstName, lastName } 37 | -- | ``` 38 | class FormDefaults a where 39 | formDefaults :: a 40 | 41 | instance formDefaultsUnit :: FormDefaults Unit where formDefaults = unit 42 | instance formDefaultsBoolean :: FormDefaults Boolean where formDefaults = false 43 | instance formDefaultsNumber :: FormDefaults Number where formDefaults = 0.0 44 | instance formDefaultsInt :: FormDefaults Int where formDefaults = 0 45 | instance formDefaultsString :: FormDefaults String where formDefaults = "" 46 | instance formDefaultsArray :: FormDefaults (Array a) where formDefaults = [] 47 | instance formDefaultsSet :: FormDefaults (Set a) where formDefaults = Set.empty 48 | instance formDefaultsMap :: FormDefaults (Map k a) where formDefaults = Map.empty 49 | instance formDefaultsObject :: FormDefaults (Object a) where formDefaults = Object.empty 50 | 51 | instance formDefaultsNonEmptyArray :: FormDefaults a => FormDefaults (NonEmptyArray a) where 52 | formDefaults = pure formDefaults 53 | 54 | instance formDefaultsProxy :: FormDefaults (Proxy a) where 55 | formDefaults = Proxy 56 | 57 | instance formDefaultsMaybe :: FormDefaults (Maybe a) where 58 | formDefaults = Nothing 59 | 60 | instance formDefaultsEither :: FormDefaults a => FormDefaults (Either a b) where 61 | formDefaults = Left formDefaults 62 | 63 | instance formDefaultsValidated :: FormDefaults a => FormDefaults (Validated a) where 64 | formDefaults = Fresh formDefaults 65 | 66 | instance formDefaultsRecord :: 67 | ( RowToList r rl 68 | , FormDefaultsRecord rl () r 69 | ) => FormDefaults (Record r) where 70 | formDefaults = Builder.build (formDefaultsRecordBuilder (Proxy :: Proxy rl)) {} 71 | 72 | -- 73 | class FormDefaultsRecord rl r_ r | rl -> r_ r where 74 | formDefaultsRecordBuilder :: Proxy rl -> Builder { | r_ } { | r } 75 | 76 | instance formDefaultsRecordNil :: FormDefaultsRecord Nil () () where 77 | formDefaultsRecordBuilder _ = identity 78 | 79 | instance formDefaultsRecordCons :: 80 | ( IsSymbol l 81 | , FormDefaults a 82 | , FormDefaultsRecord tail r_ tailR 83 | , Lacks l tailR 84 | , Cons l a tailR r 85 | ) => FormDefaultsRecord (Cons l a tail) r_ r where 86 | formDefaultsRecordBuilder _ = head <<< tail 87 | where 88 | head = Builder.insert (Proxy :: Proxy l) (formDefaults :: a) 89 | tail = formDefaultsRecordBuilder (Proxy :: Proxy tail) 90 | -------------------------------------------------------------------------------- /src/Lumi/Components/Icon.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Icon where 2 | 3 | import Prelude 4 | 5 | import Effect.Unsafe (unsafePerformEffect) 6 | import JSS (JSS, jss) 7 | import React.Basic.Classic (Component, JSX, createComponent, element, makeStateless) 8 | import React.Basic.DOM (CSS, css, unsafeCreateDOMComponent) 9 | 10 | data IconType 11 | = ArrowDown 12 | | ArrowLeft 13 | | ArrowRight 14 | | ArrowUp 15 | | Bin 16 | | Cart 17 | | Check 18 | | Close 19 | | Currency 20 | | Grid 21 | | Hamburger 22 | | Indeterminate 23 | | Info 24 | | List 25 | | Minus 26 | | Overflow 27 | | Percent 28 | | Plus 29 | | Remove 30 | | Search 31 | | Rearrange 32 | 33 | instance showIconType :: Show IconType where 34 | show ArrowDown = "↓" 35 | show ArrowLeft = "←" 36 | show ArrowRight = "→" 37 | show ArrowUp = "↑" 38 | show Bin = "🗑" 39 | show Cart = "🛒" 40 | show Check = "✓" 41 | show Close = "X" 42 | show Currency = "$" 43 | show Grid = "⌗" 44 | show Hamburger = "" 45 | show Indeterminate = "⎻" 46 | show Info = "?" 47 | show List = "" 48 | show Minus = "-" 49 | show Overflow = "⋯" 50 | show Percent = "%" 51 | show Plus = "+" 52 | show Remove = "x" 53 | show Search = "🔍" 54 | show Rearrange = "" 55 | 56 | iconArrowDown :: IconType 57 | iconArrowDown = ArrowDown 58 | 59 | iconArrowLeft :: IconType 60 | iconArrowLeft = ArrowLeft 61 | 62 | iconArrowRight :: IconType 63 | iconArrowRight = ArrowRight 64 | 65 | iconArrowUp :: IconType 66 | iconArrowUp = ArrowUp 67 | 68 | iconBin :: IconType 69 | iconBin = Bin 70 | 71 | iconCart :: IconType 72 | iconCart = Cart 73 | 74 | iconClose :: IconType 75 | iconClose = Close 76 | 77 | iconCurrency :: IconType 78 | iconCurrency = Currency 79 | 80 | iconGrid :: IconType 81 | iconGrid = Grid 82 | 83 | iconHamburger :: IconType 84 | iconHamburger = Hamburger 85 | 86 | iconIndeterminate :: IconType 87 | iconIndeterminate = Indeterminate 88 | 89 | iconInfo :: IconType 90 | iconInfo = Info 91 | 92 | iconList :: IconType 93 | iconList = List 94 | 95 | iconMinus :: IconType 96 | iconMinus = Minus 97 | 98 | iconOverflow :: IconType 99 | iconOverflow = Overflow 100 | 101 | iconPercent :: IconType 102 | iconPercent = Percent 103 | 104 | iconPlus :: IconType 105 | iconPlus = Plus 106 | 107 | iconRemove :: IconType 108 | iconRemove = Remove 109 | 110 | iconSearch :: IconType 111 | iconSearch = Search 112 | 113 | iconRearrange :: IconType 114 | iconRearrange = Rearrange 115 | 116 | type IconProps = 117 | { type_ :: IconType 118 | , style :: CSS 119 | } 120 | 121 | component :: Component IconProps 122 | component = createComponent "Icon" 123 | 124 | icon :: IconProps -> JSX 125 | icon = makeStateless component $ lumiIconElement <<< mapProps 126 | where 127 | lumiIconElement = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-font-icon") 128 | 129 | mapProps props = 130 | { "data-icon-char": show props.type_ 131 | , style: props.style 132 | } 133 | 134 | icon_ :: IconType -> JSX 135 | icon_ type_ = icon 136 | { type_ 137 | , style: css {} 138 | } 139 | 140 | styles :: JSS 141 | styles = jss 142 | { "@font-face": 143 | { fontFamily: "lumi-font-icons" 144 | , src: "url(https://s3-us-west-2.amazonaws.com/lumi-gumball/fonts/unicode/lumi-font-icons.woff)" 145 | , fontWeight: "400" 146 | , fontStyle: "normal" 147 | } 148 | 149 | , "@global": 150 | { "lumi-font-icon": 151 | { fontSize: "inherit" 152 | , color: "inherit" 153 | , lineHeight: "inherit" 154 | , "&::before": 155 | { fontFamily: "lumi-font-icons!important" 156 | , content: "attr(data-icon-char)" 157 | , fontStyle: "normal!important" 158 | , fontWeight: "normal!important" 159 | , fontVariant: "normal!important" 160 | , textTransform: "none!important" 161 | , speak: "none" 162 | , "WebkitFontSmoothing": "antialiased" 163 | , "MozOsxFontSmoothing": "grayscale" 164 | , verticalAlign: "middle" 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Lumi/Components/Input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | var LumiInputElement = function (_props) { 4 | this.inputRef = React.createRef(); 5 | return this; 6 | }; 7 | 8 | LumiInputElement.prototype = Object.create(React.Component.prototype); 9 | 10 | LumiInputElement.displayName = "LumiInputElement"; 11 | 12 | LumiInputElement.prototype.setIndeterminate = function () { 13 | if (this.inputRef.current.indeterminate !== this.props.indeterminate) { 14 | this.inputRef.current.indeterminate = this.props.indeterminate; 15 | } 16 | }; 17 | 18 | LumiInputElement.prototype.componentDidMount = function () { 19 | this.setIndeterminate(); 20 | }; 21 | 22 | LumiInputElement.prototype.componentDidUpdate = function () { 23 | this.setIndeterminate(); 24 | }; 25 | 26 | LumiInputElement.prototype.render = function () { 27 | var this_ = this; 28 | var props = {}; 29 | Object.keys(this.props).forEach(function (key) { 30 | if (key === "indeterminate") return; 31 | props[key] = this_.props[key]; 32 | }); 33 | props.ref = this.inputRef; 34 | return React.createElement("input", props); 35 | }; 36 | 37 | export const lumiInputElement = LumiInputElement; 38 | -------------------------------------------------------------------------------- /src/Lumi/Components/InputGroup.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.InputGroup where 2 | 3 | import Prelude 4 | 5 | import Color (cssStringHSLA) 6 | import Data.Array (fromFoldable) 7 | import Data.Nullable (Nullable, toMaybe) 8 | import Effect.Unsafe (unsafePerformEffect) 9 | import JSS (JSS, jss) 10 | import Lumi.Components.Color (colors) 11 | import Lumi.Components.ZIndex (ziInputGroup) 12 | import React.Basic.Classic (Component, JSX, createComponent, element, makeStateless) 13 | import React.Basic.DOM (CSS, unsafeCreateDOMComponent) 14 | import React.Basic.DOM as R 15 | 16 | type InputGroupProps = 17 | { addOnLeft :: Nullable JSX 18 | , addOnRight :: Nullable JSX 19 | , inputContent :: Nullable JSX 20 | , style :: CSS 21 | } 22 | 23 | component :: Component InputGroupProps 24 | component = createComponent "InputGroup" 25 | 26 | inputGroup :: InputGroupProps -> JSX 27 | inputGroup = makeStateless component render 28 | where 29 | render props = 30 | inputGroupElement 31 | { style: props.style 32 | , children: 33 | [ inputGroupAddOn { children: fromFoldable (toMaybe props.addOnLeft) } 34 | , R.a { className: "input-container", children: fromFoldable (toMaybe props.inputContent) } 35 | , inputGroupAddOn { children : fromFoldable (toMaybe props.addOnRight) } 36 | ] 37 | } 38 | 39 | inputGroupElement = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-input-group") 40 | inputGroupAddOn = element (unsafePerformEffect $ unsafeCreateDOMComponent "input-group-addon") 41 | 42 | styles :: JSS 43 | styles = jss 44 | { "@global": 45 | { "lumi-input-group": 46 | { display: "flex" 47 | , width: "100%" 48 | 49 | , "& a.input-container": 50 | { flex: "1" 51 | , marginRight: "-1px" 52 | , "&:focus, &:hover": { zIndex: ziInputGroup } 53 | , "& input[type=\"text\"], & input.lumi[type=\"text\"]": 54 | { borderRadius: "0" 55 | , width: "100%" 56 | } 57 | } 58 | 59 | , "& input-group-addon:first-child button.lumi": 60 | { marginRight: "-1px" 61 | , borderTopRightRadius: "0" 62 | , borderBottomRightRadius: "0" 63 | , borderTopLeftRadius: buttonBorderRadius 64 | , borderBottomLeftRadius: buttonBorderRadius 65 | } 66 | , "& input:not(:first-child) button.lumi": 67 | { borderTopLeftRadius: "0" 68 | , borderBottomLeftRadius: "0" 69 | } 70 | , "& input:not(:last-child) button.lumi": 71 | { marginRight: "-1px" 72 | , borderTopRightRadius: "0" 73 | , borderBottomRightRadius: "0" 74 | } 75 | , "& input-group-addon:last-child button.lumi": 76 | { borderTopRightRadius: buttonBorderRadius 77 | , borderBottomRightRadius: buttonBorderRadius 78 | , borderTopLeftRadius:"0" 79 | , borderBottomLeftRadius:"0" 80 | } 81 | 82 | , "& input-group-addon button.lumi": 83 | { "&[data-color=\"secondary\"]": 84 | { backgroundColor: cssStringHSLA colors.transparent 85 | } 86 | , "&:focus, &:hover": { zIndex: ziInputGroup } 87 | } 88 | } 89 | } 90 | } 91 | where 92 | buttonBorderRadius = "3px" 93 | -------------------------------------------------------------------------------- /src/Lumi/Components/Layouts.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Layouts where 2 | 3 | import Color (cssStringHSLA) 4 | import JSS (JSS, jss) 5 | import Lumi.Components.Color (colors) 6 | 7 | styles :: JSS 8 | styles = jss 9 | { "@global": 10 | { "lumi-layout": 11 | { boxSizing: "border-box" 12 | , flex: "1" 13 | , display: "flex" 14 | , flexFlow: "column" 15 | , alignSelf: "stretch" 16 | 17 | , "& > lumi-layout-view-head": 18 | { boxSizing: "border-box" 19 | , flex: "none" 20 | , display: "flex" 21 | , flexDirection: "row" 22 | , padding: "12px 24px" 23 | , backgroundColor: cssStringHSLA colors.black6 24 | , borderBottom: [ "1px", "solid", cssStringHSLA colors.black4 ] 25 | } 26 | 27 | , "& > lumi-layout-view-body": 28 | { boxSizing: "border-box" 29 | , overflowY: "auto" 30 | , display: "flex" 31 | , flexFlow: "column" 32 | } 33 | } 34 | 35 | , "lumi-layout-centered": 36 | { display: "block" 37 | , width: "850px" 38 | , maxWidth: "90%" 39 | , margin: "5vh auto" 40 | } 41 | 42 | , "lumi-layout-centered-full-width": 43 | { display: "block" 44 | , width: "90%" 45 | , maxWidth: "90%" 46 | , margin: "5vh auto" 47 | } 48 | 49 | , "lumi-layout > lumi-layout-view-body, lumi-layout-centered, .lumi-layout-content": 50 | { "& lumi-section-header": 51 | { paddingTop: "32px" 52 | , paddingBottom: "12px" 53 | } 54 | } 55 | 56 | , "lumi-layout-view-body, lumi-layout-centered": 57 | { flex: "1" 58 | , "& lumi-section-header": 59 | { paddingTop: "32px" 60 | , paddingBottom: "12px" 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Lumi/Components/Layouts/Centered.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Layouts.Centered where 2 | 3 | import Prelude 4 | 5 | import Effect.Unsafe (unsafePerformEffect) 6 | import React.Basic.Classic (JSX, element) 7 | import React.Basic.DOM (unsafeCreateDOMComponent) 8 | 9 | layout :: JSX -> JSX 10 | layout = \content -> 11 | lumiLayoutCentered 12 | { children: [content] 13 | } 14 | where 15 | lumiLayoutCentered = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-layout-centered") 16 | 17 | layoutFullWidth :: JSX -> JSX 18 | layoutFullWidth = \content -> 19 | lumiLayoutCentered 20 | { children: [content] 21 | } 22 | where 23 | lumiLayoutCentered = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-layout-centered-full-width") 24 | -------------------------------------------------------------------------------- /src/Lumi/Components/Layouts/OneColumnWithHeader.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Layouts.OneColumnWithHeader where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Effect.Unsafe (unsafePerformEffect) 7 | import Lumi.Components.Text (h2, text) 8 | import React.Basic.Classic (Component, JSX, createComponent, element, fragment, makeStateless) 9 | import React.Basic.DOM (unsafeCreateDOMComponent) 10 | import React.Basic.DOM as R 11 | 12 | component :: Component LayoutProps 13 | component = createComponent "OneColumnWithHeader" 14 | 15 | type LayoutProps = 16 | { titleContent :: JSX 17 | , additionalHeaderContent :: JSX 18 | , actionContent :: JSX 19 | , mainContent :: JSX 20 | , sidebarContent :: Maybe JSX 21 | } 22 | 23 | layout :: LayoutProps -> JSX 24 | layout = makeStateless component render 25 | where 26 | render { titleContent, additionalHeaderContent, actionContent, mainContent, sidebarContent } = 27 | lumiLayout 28 | { children: 29 | [ lumiLayoutViewHead 30 | { children: 31 | [ text $ h2 32 | { children = [titleContent] 33 | , style = R.css 34 | { paddingBottom: "0" 35 | , fontWeight: "normal" 36 | , marginRight: "12px" 37 | , flex: "none" 38 | } 39 | } 40 | , R.span 41 | { style: R.css 42 | { overflow: "visible" 43 | , flex: "1 1 70%" 44 | , wordWrap: "break-word" 45 | , padding: "0" 46 | } 47 | , children: [additionalHeaderContent] 48 | } 49 | , R.div 50 | { style: R.css { flex: "1 1 10px" } 51 | } 52 | , R.div 53 | { style: R.css { flex: "none" } 54 | , children: [actionContent] 55 | } 56 | ] 57 | } 58 | , lumiLayoutViewBody 59 | { className: "view-body view-scroll" 60 | , children: case sidebarContent of 61 | Nothing -> 62 | [ mainContent ] 63 | Just sbc -> 64 | [ R.div 65 | { className: "row row-justify-between row-no-padding" 66 | , children: 67 | [ R.div { className: "column view-scroll", children: [ mainContent]} 68 | , sidebarLayout { content: sbc } 69 | ] 70 | } 71 | ] 72 | } 73 | ] 74 | } 75 | 76 | lumiLayout = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-layout") 77 | lumiLayoutViewHead = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-layout-view-head") 78 | lumiLayoutViewBody = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-layout-view-body") 79 | 80 | sidebarLayoutComponent :: Component SidebarLayoutProps 81 | sidebarLayoutComponent = createComponent "Sidebar" 82 | 83 | type SidebarLayoutProps = 84 | { content :: JSX 85 | } 86 | 87 | sidebarLayout :: SidebarLayoutProps -> JSX 88 | sidebarLayout = makeStateless sidebarLayoutComponent render where 89 | render { content } = 90 | fragment $ 91 | [ R.div 92 | { style: R.css { "flex": "0 0 30%" } 93 | , className: "column bl view-scroll" 94 | , maxLength: 400 95 | , children: [ 96 | R.div { className: "ppa", children: [ content ]} 97 | ] 98 | } 99 | ] 100 | -------------------------------------------------------------------------------- /src/Lumi/Components/Layouts/Tabs.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Layouts.Tabs where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Data.NonEmpty (NonEmpty, oneOf) 7 | import Effect (Effect) 8 | import Lumi.Components.Tab (TabId, TabKey) 9 | import Lumi.Components.Tab as Tab 10 | import React.Basic.Classic (Component, JSX, createComponent, makeStateless) 11 | import React.Basic.DOM as R 12 | import Web.HTML.History (URL) 13 | 14 | component :: Component TabLayoutProps 15 | component = createComponent "TabsLayout" 16 | 17 | type TabLayoutProps = 18 | { currentLocation :: URL 19 | , queryKey :: TabKey 20 | , useHash :: Boolean 21 | , navigate :: Maybe (URL -> Effect Unit) 22 | , tabs :: NonEmpty Array 23 | { id :: TabId 24 | , label :: String 25 | , count :: Maybe Int 26 | , content :: Unit -> JSX 27 | } 28 | } 29 | 30 | tabLayout :: TabLayoutProps -> JSX 31 | tabLayout = makeStateless component render 32 | where 33 | render { currentLocation, navigate, queryKey, tabs, useHash } = 34 | Tab.tabs 35 | { style: R.css { "paddingLeft": "24px" } 36 | , tabStyle: R.css {} 37 | , selectedTabStyle: R.css {} 38 | , currentLocation 39 | , queryKey 40 | , useHash 41 | , navigate 42 | , tabs: oneOf tabs <#> \t -> 43 | { content: t.content 44 | , id: t.id 45 | , label: t.label 46 | , count: t.count 47 | , testId: Nothing 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Lumi/Components/Link.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Link where 2 | 3 | import Prelude 4 | 5 | import Color (cssStringHSLA) 6 | import Data.Foldable (foldMap) 7 | import Data.Maybe (Maybe(..)) 8 | import Data.Newtype (un) 9 | import Data.Nullable (toNullable) 10 | import Data.String (Pattern(..), contains) 11 | import Effect (Effect) 12 | import Effect.Uncurried (runEffectFn1) 13 | import Effect.Unsafe (unsafePerformEffect) 14 | import JSS (JSS, jss) 15 | import Lumi.Components.Color (colors) 16 | import React.Basic.Classic (Component, JSX, createComponent, element, empty, makeStateless) 17 | import React.Basic.DOM (CSS, css, unsafeCreateDOMComponent) 18 | import React.Basic.DOM as R 19 | import React.Basic.DOM.Events (altKey, button, ctrlKey, metaKey, preventDefault, shiftKey, stopPropagation) 20 | import React.Basic.Events (handler, merge, syntheticEvent) 21 | import Web.HTML.History (URL(..)) 22 | 23 | 24 | type LinkProps = 25 | { className :: Maybe String 26 | , href :: URL 27 | , navigate :: Maybe (Effect Unit) 28 | , style :: CSS 29 | , target :: Maybe String 30 | , testId :: Maybe String 31 | , text :: JSX 32 | } 33 | 34 | component :: Component LinkProps 35 | component = createComponent "Link" 36 | 37 | link :: LinkProps -> JSX 38 | link = makeStateless component render 39 | where 40 | lumiAnchorElement = element (unsafePerformEffect $ unsafeCreateDOMComponent "a") 41 | 42 | render { className, href, navigate, style, target, testId, text } = 43 | lumiAnchorElement 44 | { children: text 45 | , className: "lumi" <> foldMap (" " <> _) className 46 | , "data-testid": toNullable testId 47 | , href: un URL href 48 | , onClick: handler (merge { button, metaKey, altKey, ctrlKey, shiftKey, syntheticEvent }) 49 | \{ button, metaKey, altKey, ctrlKey, shiftKey, syntheticEvent } -> do 50 | case navigate, button, metaKey, altKey, ctrlKey, shiftKey of 51 | Just n', Just 0, Just false, Just false, Just false, Just false -> 52 | runEffectFn1 53 | (handler (stopPropagation <<< preventDefault) $ const n') 54 | syntheticEvent 55 | _ , _ , _ , _ , _ , _ -> 56 | runEffectFn1 57 | (handler stopPropagation mempty) 58 | syntheticEvent 59 | , style 60 | , target: toNullable target 61 | } 62 | 63 | defaults :: LinkProps 64 | defaults = 65 | { className: Nothing 66 | , href: URL "" 67 | , navigate: Nothing 68 | , style: css {} 69 | , target: Nothing 70 | , testId: Nothing 71 | , text: empty 72 | } 73 | 74 | styles :: JSS 75 | styles = jss 76 | { "@global": 77 | { "a.lumi": 78 | { color: cssStringHSLA colors.primary 79 | , textDecoration: "none" 80 | , "&:visited": 81 | { color: cssStringHSLA colors.primary 82 | , textDecoration: "none" 83 | } 84 | , "&:hover": 85 | { cursor: "pointer" 86 | , textDecoration: "underline" 87 | } 88 | , "&:focus, &:active": { outline: "0" } 89 | } 90 | } 91 | } 92 | 93 | linkOut 94 | :: { href :: URL 95 | , label :: String 96 | } 97 | -> JSX 98 | linkOut { label, href: (URL href) } = 99 | link defaults 100 | { className = Just "action" 101 | , href = href' 102 | , text = R.text label 103 | , target = Just "_blank" 104 | } 105 | where href' = URL if contains (Pattern "http") href then href else "https://" <> href -------------------------------------------------------------------------------- /src/Lumi/Components/Loader.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Loader where 2 | 3 | import Prelude 4 | 5 | import Color (cssStringHSLA) 6 | import Data.Nullable (Nullable) 7 | import Effect.Unsafe (unsafePerformEffect) 8 | import JSS (JSS, jss) 9 | import Lumi.Components.Color (Color, colors) 10 | import React.Basic.Classic (Component, JSX, createComponent, element, makeStateless) 11 | import React.Basic.DOM (CSS, unsafeCreateDOMComponent) 12 | 13 | type LoaderProps 14 | = { style :: CSS 15 | , testId :: Nullable String 16 | } 17 | 18 | component :: Component LoaderProps 19 | component = createComponent "Loader" 20 | 21 | loader :: LoaderProps -> JSX 22 | loader = makeStateless component $ loaderElement <<< mapProps 23 | where 24 | loaderElement = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-loader") 25 | 26 | mapProps props = 27 | { style: props.style 28 | , "data-testid": props.testId 29 | } 30 | 31 | styles :: JSS 32 | styles = 33 | jss 34 | { "@global": 35 | { "lumi-loader": 36 | spinnerMixin 37 | { color: colors.black1 38 | , highlightColor: colors.black4 39 | , radius: "38px" 40 | , borderWidth: "5px" 41 | } 42 | , "@keyframes spin": 43 | { from: { transform: "rotate(0deg)" } 44 | , to: { transform: "rotate(360deg)" } 45 | } 46 | } 47 | } 48 | 49 | spinnerMixin :: 50 | { color :: Color 51 | , highlightColor :: Color 52 | , radius :: String 53 | , borderWidth :: String 54 | } -> 55 | JSS 56 | spinnerMixin { color: c, highlightColor, radius, borderWidth } = 57 | jss 58 | { boxSizing: "border-box" 59 | , content: "\"\"" 60 | , display: "inline-block" 61 | , height: radius 62 | , width: radius 63 | , border: [ borderWidth, "solid", cssStringHSLA c ] 64 | , borderTopColor: cssStringHSLA highlightColor 65 | , borderRadius: "50%" 66 | , animation: "spin 1s infinite linear" 67 | , animationName: "spin" 68 | } 69 | -------------------------------------------------------------------------------- /src/Lumi/Components/Modal.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react'; 4 | var h = React.createElement; 5 | import ReactDOM from 'react-dom'; 6 | var modalRoot = document.getElementById("modal-root"); 7 | 8 | function assign(target, from) { 9 | for (var nextKey in from) { 10 | if (Object.prototype.hasOwnProperty.call(from, nextKey)) { 11 | target[nextKey] = from[nextKey]; 12 | } 13 | } 14 | return target; 15 | } 16 | 17 | export const modalBuilder = function (ModalPortal) { 18 | var ModalComponent = function constructor(_props) { 19 | this.el = document.createElement("div"); 20 | var this_ = this; 21 | this.requestClose = function (event) { 22 | if (this_.ownerHandlesClose()) { 23 | this_.props.onRequestClose(event); 24 | } 25 | }; 26 | this.ownerHandlesClose = function () { 27 | return !!this_.props.onRequestClose; 28 | }; 29 | this.portalRef = function (ref) { 30 | this_.portal = ref; 31 | }; 32 | return this; 33 | }; 34 | ModalComponent.prototype = Object.create(React.Component.prototype); 35 | ModalComponent.displayName = "Modal"; 36 | 37 | ModalComponent.prototype.componentDidMount = function () { 38 | modalRoot.appendChild(this.el); 39 | }; 40 | 41 | ModalComponent.prototype.componentWillUnmount = function () { 42 | modalRoot.removeChild(this.el); 43 | }; 44 | 45 | ModalComponent.prototype.render = function () { 46 | return ReactDOM.createPortal( 47 | h( 48 | ModalPortal, 49 | assign( 50 | { 51 | ref: this.portalRef, 52 | requestClose: this.requestClose 53 | }, 54 | this.props 55 | ) 56 | ), 57 | this.el 58 | ); 59 | }; 60 | 61 | return ModalComponent; 62 | }; 63 | 64 | export const toggleBodyClass = function (className, on) { 65 | if (on) { 66 | document.body.classList.add(className); 67 | } else { 68 | document.body.classList.remove(className); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /src/Lumi/Components/Navigation.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const checkIsEventTargetInTree = function (domNode, e) { 4 | return e.target === domNode || domNode.contains(e.target); 5 | }; 6 | -------------------------------------------------------------------------------- /src/Lumi/Components/Orientation.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Orientation where 2 | 3 | import Prelude 4 | 5 | data Orientation 6 | = Horizontal 7 | | Vertical 8 | 9 | derive instance eqOrientation :: Eq Orientation 10 | 11 | instance showOrientation :: Show Orientation where 12 | show Horizontal = "horizontal" 13 | show Vertical = "vertical" 14 | -------------------------------------------------------------------------------- /src/Lumi/Components/Responsive.js: -------------------------------------------------------------------------------- 1 | export { useMediaPredicate as useMedia_ } from "react-media-hook"; 2 | -------------------------------------------------------------------------------- /src/Lumi/Components/Responsive.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Responsive where 2 | 3 | import Prelude 4 | 5 | import Effect.Uncurried (EffectFn1, runEffectFn1) 6 | import Effect.Unsafe (unsafePerformEffect) 7 | import React.Basic.Hooks (Component, Hook, JSX, component, unsafeHook) 8 | import React.Basic.Hooks as React 9 | 10 | foreign import data MediaQuery :: Type 11 | 12 | foreign import data UseMediaQuery :: Type -> Type 13 | 14 | useMedia :: String -> Hook UseMediaQuery Boolean 15 | useMedia mq = unsafeHook (runEffectFn1 useMedia_ mq) 16 | 17 | foreign import useMedia_ :: EffectFn1 String Boolean 18 | 19 | useIsPhone :: Hook UseMediaQuery Boolean 20 | useIsPhone = map not (useMedia ("(min-width: " <> show minWidthNonPhone <> "px)")) 21 | 22 | useIsMobile :: Hook UseMediaQuery Boolean 23 | useIsMobile = map not useIsDesktop 24 | 25 | useIsDesktop :: Hook UseMediaQuery Boolean 26 | useIsDesktop = useMedia ("(min-width: " <> show minWidthDesktop <> "px)") 27 | 28 | -- | Prefer `useIsPhone` or `phone` to using this value 29 | -- | directly. Named in the negative because -- | this 30 | -- | width is the first size that is _not_ a phone and 31 | -- | trying to use this width as a "max-width" leaves 32 | -- | out fractional pixels (447.5px) which sometimes 33 | -- | occur on high-dpi displays. 34 | minWidthNonPhone :: Int 35 | minWidthNonPhone = 448 36 | 37 | minWidthDesktop :: Int 38 | minWidthDesktop = 860 39 | 40 | mobile :: (Unit -> JSX) -> JSX 41 | mobile = \render -> 42 | c \isMobile -> if isMobile then render unit else mempty 43 | -- WARNING: Don't add any arguments to the definition of `mobile`! 44 | -- `c` must be fully applied at module creation! 45 | where 46 | c = unsafePerformEffect (mkMediaComponent "Mobile" useIsMobile) 47 | 48 | withMobile :: (Boolean -> JSX) -> JSX 49 | withMobile = c 50 | -- WARNING: Don't add any arguments to the `withMobile` function! 51 | -- `c` must be fully applied at module creation! 52 | where 53 | c = unsafePerformEffect (mkMediaComponent "Mobile" useIsMobile) 54 | 55 | phone :: (Unit -> JSX) -> JSX 56 | phone = \render -> 57 | c \isPhone -> if isPhone then render unit else mempty 58 | -- WARNING: Don't add any arguments to the definition of `phone`! 59 | -- `c` must be fully applied at module creation! 60 | where 61 | c = unsafePerformEffect (mkMediaComponent "Phone" useIsPhone) 62 | 63 | withPhone :: (Boolean -> JSX) -> JSX 64 | withPhone = c 65 | -- WARNING: Don't add any arguments to the `withPhone` function! 66 | -- `c` must be fully applied at module creation! 67 | where 68 | c = unsafePerformEffect (mkMediaComponent "Phone" useIsPhone) 69 | 70 | desktop :: (Unit -> JSX) -> JSX 71 | desktop = \render -> 72 | c \isDesktop -> if isDesktop then render unit else mempty 73 | -- WARNING: Don't add any arguments the `desktop` function! 74 | -- `c` must be fully applied at module creation! 75 | where 76 | c = unsafePerformEffect (mkMediaComponent "Desktop" useIsDesktop) 77 | 78 | withDesktop :: (Boolean -> JSX) -> JSX 79 | withDesktop = c 80 | -- WARNING: Don't add any arguments to the `withDesktop` function! 81 | -- `c` must be fully applied at module creation! 82 | where 83 | c = unsafePerformEffect (mkMediaComponent "Desktop" useIsDesktop) 84 | 85 | mkMediaComponent :: 86 | String -> 87 | Hook UseMediaQuery Boolean -> 88 | Component (Boolean -> JSX) 89 | mkMediaComponent name umq = do 90 | component ("MediaQuery(" <> name <> ")") \render -> React.do 91 | isMatch <- umq 92 | pure $ render isMatch 93 | -------------------------------------------------------------------------------- /src/Lumi/Components/Row.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Row where 2 | 3 | import Prelude 4 | 5 | import Effect.Unsafe (unsafePerformEffect) 6 | import JSS (JSS, jss) 7 | import React.Basic.Classic (Component, JSX, createComponent, element, makeStateless) 8 | import React.Basic.DOM (CSS, css, unsafeCreateDOMComponent) 9 | 10 | component :: Component RowProps 11 | component = createComponent "Row" 12 | 13 | type RowProps = 14 | { children :: Array JSX 15 | , style :: CSS 16 | } 17 | 18 | row :: RowProps -> JSX 19 | row = makeStateless component lumiRowElement 20 | where 21 | lumiRowElement = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-row") 22 | 23 | row_ :: Array JSX -> JSX 24 | row_ children = row { children, style: css {} } 25 | 26 | responsiveRow :: RowProps -> JSX 27 | responsiveRow = makeStateless component lumiResponsiveRowElement 28 | where 29 | lumiResponsiveRowElement = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-responsive-row") 30 | 31 | responsiveRow_ :: Array JSX -> JSX 32 | responsiveRow_ children = responsiveRow { children, style: css {} } 33 | 34 | styles :: JSS 35 | styles = jss 36 | { "@global": 37 | { "lumi-row": 38 | { boxSizing: "border-box" 39 | , display: "flex" 40 | , flexDirection: "row" 41 | } 42 | 43 | , "lumi-responsive-row": 44 | { boxSizing: "border-box" 45 | , display: "flex" 46 | , flexDirection: "row" 47 | 48 | , "@media (max-width: 860px)": 49 | { flexDirection: "column" 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Lumi/Components/Size.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Size where 2 | 3 | import Prelude 4 | 5 | 6 | data Size 7 | = Small 8 | | Medium 9 | | Large 10 | | ExtraLarge 11 | | ExtraExtraLarge 12 | 13 | derive instance eqSize :: Eq Size 14 | derive instance ordSize :: Ord Size 15 | 16 | instance showSize :: Show Size where 17 | show Small = "small" 18 | show Medium = "medium" 19 | show Large = "large" 20 | show ExtraLarge = "extra-large" 21 | show ExtraExtraLarge = "extra-extra-large" 22 | 23 | small :: Size 24 | small = Small 25 | 26 | medium :: Size 27 | medium = Medium 28 | 29 | large :: Size 30 | large = Large 31 | 32 | extraLarge :: Size 33 | extraLarge = ExtraLarge 34 | 35 | extraExtraLarge :: Size 36 | extraExtraLarge = ExtraExtraLarge 37 | -------------------------------------------------------------------------------- /src/Lumi/Components/Slider.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Slider where 2 | 3 | import Prelude 4 | 5 | import Color (cssStringHSLA) 6 | import Data.Int (fromString) 7 | import Data.Maybe (Maybe(..), fromMaybe) 8 | import Data.Nullable (toNullable) 9 | import Effect.Uncurried (EffectFn1, runEffectFn1) 10 | import Effect.Unsafe (unsafePerformEffect) 11 | import JSS (JSS, jss) 12 | import Lumi.Components.Color (colors) 13 | import Lumi.Components.Input as Input 14 | import React.Basic.Classic (Component, JSX, createComponent, element, makeStateless) 15 | import React.Basic.DOM as R 16 | import React.Basic.DOM.Events (targetValue) 17 | import React.Basic.Events as Events 18 | 19 | type SliderProps = 20 | { completed :: Int 21 | , onChange :: EffectFn1 Int Unit 22 | , style :: R.CSS 23 | } 24 | 25 | component :: Component SliderProps 26 | component = createComponent "Slider" 27 | 28 | slider :: SliderProps -> JSX 29 | slider = makeStateless component render 30 | where 31 | render props = 32 | lumiSliderElement 33 | { style: props.style 34 | , children: 35 | [ Input.input Input.range 36 | { value = show props.completed 37 | , min = toNullable $ Just 0.0 38 | , max = toNullable $ Just 100.0 39 | , onChange = Events.handler targetValue \targetValue -> do 40 | runEffectFn1 props.onChange $ fromMaybe 0 $ fromString =<< targetValue 41 | } 42 | , lumiValueBarElement 43 | { style: R.css { width: show props.completed <> "%" } 44 | } 45 | ] 46 | } 47 | 48 | lumiSliderElement = element $ unsafePerformEffect $ R.unsafeCreateDOMComponent "lumi-slider" 49 | lumiValueBarElement = element $ unsafePerformEffect $ R.unsafeCreateDOMComponent "lumi-value-bar" 50 | 51 | styles :: JSS 52 | styles = jss 53 | { "@global": 54 | { "lumi-slider": 55 | { position: "relative" 56 | , width: "100%" 57 | 58 | , "& input.lumi[type=\"range\"]": 59 | { position: "absolute" 60 | , left: "0px" 61 | , padding: "0px" 62 | , margin: "0px" 63 | 64 | , width: "100%" 65 | , height: "4px" 66 | } 67 | , "& lumi-value-bar": 68 | { position: "absolute" 69 | , padding: "0px" 70 | , margin: "0px" 71 | 72 | , backgroundColor: cssStringHSLA colors.primary 73 | , height: "4px" 74 | , pointerEvents: "none" 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Lumi/Components/Spacing.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Spacing 2 | ( Space(..) 3 | , hspace 4 | , hspaceWithStyle 5 | , vspace 6 | , toPixels 7 | ) where 8 | 9 | import Prelude 10 | 11 | import React.Basic.Classic (JSX) 12 | import React.Basic.DOM as R 13 | import React.Basic.Emotion (class IsStyleProperty, str) 14 | 15 | data Space 16 | = S4 | S8 | S12 | S16 | S24 | S32 | S40 | S48 | S56 17 | | S64 | S72 | S80 | S88 | S96 | S104 | S112 18 | 19 | hspaceWithStyle :: R.CSS -> Space -> JSX 20 | hspaceWithStyle sty size = 21 | R.div 22 | { style: sty <> R.css 23 | { paddingLeft: toPixels size 24 | } 25 | } 26 | 27 | hspace :: Space -> JSX 28 | hspace = hspaceWithStyle mempty 29 | 30 | vspace :: Space -> JSX 31 | vspace size = 32 | R.div 33 | { style: R.css 34 | { paddingTop: toPixels size 35 | } 36 | } 37 | 38 | toPixels :: Space -> String 39 | toPixels S4 = "4px" 40 | toPixels S8 = "8px" 41 | toPixels S12 = "12px" 42 | toPixels S16 = "16px" 43 | toPixels S24 = "24px" 44 | toPixels S32 = "32px" 45 | toPixels S40 = "40px" 46 | toPixels S48 = "48px" 47 | toPixels S56 = "56px" 48 | toPixels S64 = "64px" 49 | toPixels S72 = "72px" 50 | toPixels S80 = "80px" 51 | toPixels S88 = "88px" 52 | toPixels S96 = "96px" 53 | toPixels S104 = "104px" 54 | toPixels S112 = "112px" 55 | 56 | instance isStylePropertySpace :: IsStyleProperty Space where 57 | prop = str <<< toPixels 58 | -------------------------------------------------------------------------------- /src/Lumi/Components/Status.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Status where 2 | 3 | import Prelude 4 | 5 | data Status 6 | = Active 7 | | Warning 8 | | Error 9 | | Unknown 10 | 11 | derive instance eqStatus :: Eq Status 12 | 13 | instance showStatus :: Show Status where 14 | show Active = "active" 15 | show Warning = "warning" 16 | show Error = "error" 17 | show Unknown = "unknown" 18 | 19 | active :: Status 20 | active = Active 21 | 22 | warning :: Status 23 | warning = Warning 24 | 25 | error :: Status 26 | error = Error 27 | 28 | unknown :: Status 29 | unknown = Unknown 30 | -------------------------------------------------------------------------------- /src/Lumi/Components/StatusSlat.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.StatusSlat where 2 | 3 | -- TODO remove this in favor of Lumi.Common.Components.StatusSlat or vice-versa. 4 | -- We probably don't need both 5 | 6 | import Prelude 7 | 8 | import Color (cssStringHSLA) 9 | import Data.Nullable (Nullable, toNullable, toMaybe) 10 | import Effect.Unsafe (unsafePerformEffect) 11 | import JSS (JSS, jss) 12 | import Lumi.Components.Color (colors) 13 | import Lumi.Components.Column (column) 14 | import Lumi.Components.Row (row) 15 | import Lumi.Components.Status (Status) 16 | import Lumi.Components.Text (subtext_, title_) 17 | import React.Basic.Classic (Component, JSX, createComponent, element, makeStateless) 18 | import React.Basic.DOM (css, unsafeCreateDOMComponent) 19 | 20 | type StatusCellProps = 21 | { data :: Array 22 | { label :: String 23 | , content :: String 24 | , status :: Nullable Status 25 | } 26 | } 27 | 28 | component :: Component StatusCellProps 29 | component = createComponent "StatusSlat" 30 | 31 | statusSlat :: StatusCellProps -> JSX 32 | statusSlat = makeStateless component $ lumiStatusSlat <<< mapProps 33 | where 34 | mapProps props = 35 | { className: "lumi" 36 | , children: 37 | row 38 | { style: css 39 | { height: "100%" } 40 | , children: map toStatusCell props.data 41 | } 42 | } 43 | 44 | toStatusCell { label, content, status } = 45 | lumiStatusSlatCell 46 | { "data-status": toNullable (map show (toMaybe status)) 47 | , children: 48 | column 49 | { style: css 50 | { height: "100%" 51 | , justifyContent: "center" 52 | } 53 | , children: 54 | [ subtext_ label 55 | , title_ content 56 | ] 57 | } 58 | } 59 | 60 | lumiStatusSlat = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-status-slat") 61 | lumiStatusSlatCell = element (unsafePerformEffect $ unsafeCreateDOMComponent "lumi-status-slat-cell") 62 | 63 | styles :: JSS 64 | styles = jss 65 | { "@global": 66 | { "lumi-status-slat": 67 | { border: [ "1px", "solid", cssStringHSLA colors.black4 ] 68 | , width: "100%" 69 | , height: "64px" 70 | , display: "inline-block" 71 | , borderRadius: "5px" 72 | , "& lumi-status-slat-cell": 73 | { borderRight: [ "1px", "solid", cssStringHSLA colors.black4 ] 74 | , height: "100%" 75 | , minWidth: "calc(25% - (2 * 16px))" 76 | , display: "inline-block" 77 | , padding: "0 16px" 78 | , whiteSpace: "nowrap" 79 | , overflow: "hidden" 80 | , textOverflow: "ellipsis" 81 | , "&:last-child": 82 | { borderRight: "none" 83 | } 84 | , "&[data-status=\"active\"]": 85 | { color: cssStringHSLA colors.accent1 86 | } 87 | , "&[data-status=\"warning\"]": 88 | { color: cssStringHSLA colors.accent2 89 | } 90 | , "&[data-status=\"error\"]": 91 | { color: cssStringHSLA colors.accent3 92 | } 93 | , "&[data-status=\"unknown\"]": 94 | { color: cssStringHSLA colors.black1 95 | } 96 | } 97 | } 98 | 99 | 100 | 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Lumi/Components/Styles.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Styles where 2 | 3 | import Prelude 4 | 5 | import Color (cssStringHSLA) 6 | import Data.Traversable (traverse_) 7 | import Effect (Effect) 8 | import JSS (JSS, jss) 9 | import JSS as JSS 10 | import Lumi.Components.Badge as Badge 11 | import Lumi.Components.Border as Border 12 | import Lumi.Components.Breadcrumb as Breadcrumb 13 | import Lumi.Components.Button (styles) as Button 14 | import Lumi.Components.ButtonGroup as ButtonGroup 15 | import Lumi.Components.Card as Card 16 | import Lumi.Components.CardGrid as CardGrid 17 | import Lumi.Components.Color (colors) 18 | import Lumi.Components.Column as Column 19 | import Lumi.Components.Details as Details 20 | import Lumi.Components.Divider as Divider 21 | import Lumi.Components.DropdownButton as DropdownButton 22 | import Lumi.Components.EditableTable as EditableTable 23 | import Lumi.Components.Form as Form 24 | import Lumi.Components.Icon as Icon 25 | import Lumi.Components.Images as Images 26 | import Lumi.Components.Input as Input 27 | import Lumi.Components.InputGroup as InputGroup 28 | import Lumi.Components.LabeledField as LabeledField 29 | import Lumi.Components.Layouts as Layouts 30 | import Lumi.Components.Link as Link 31 | import Lumi.Components.List as List 32 | import Lumi.Components.Loader as Loader 33 | import Lumi.Components.Lockup as Lockup 34 | import Lumi.Components.Modal as Modal 35 | import Lumi.Components.NativeSelect as NativeSelect 36 | import Lumi.Components.Navigation as Navigation 37 | import Lumi.Components.Pagination as Pagination 38 | import Lumi.Components.Pill as Pill 39 | import Lumi.Components.Row as Row 40 | import Lumi.Components.Select as Select 41 | import Lumi.Components.Slider as Slider 42 | import Lumi.Components.StatusSlat as StatusSlat 43 | import Lumi.Components.Tab as Tab 44 | import Lumi.Components.Table as Table 45 | import Lumi.Components.Text as Text 46 | import Lumi.Components.Textarea as Textarea 47 | import Lumi.Components.Toast as Toast 48 | import Lumi.Components.Tooltip as Tooltip 49 | import Lumi.Components.Upload as Upload 50 | 51 | attachGlobalComponentStyles :: Effect Unit 52 | attachGlobalComponentStyles = do 53 | jssInstance <- JSS.createInstance JSS.preset 54 | traverse_ (JSS.globalAttachStyleSheet <=< JSS.createStyleSheet jssInstance) 55 | [ globals 56 | , Badge.styles 57 | , Border.styles 58 | , Breadcrumb.styles 59 | , Button.styles 60 | , ButtonGroup.styles 61 | , Card.styles 62 | , CardGrid.styles 63 | , Column.styles 64 | , Details.styles 65 | , Divider.styles 66 | , DropdownButton.styles 67 | , EditableTable.styles 68 | , Form.styles 69 | , Icon.styles 70 | , Images.styles 71 | , Input.styles 72 | , InputGroup.styles 73 | , LabeledField.styles 74 | , Layouts.styles 75 | , Link.styles 76 | , List.styles 77 | , Loader.styles 78 | , Lockup.styles 79 | , Modal.styles 80 | , NativeSelect.styles 81 | , Navigation.styles 82 | , Pagination.styles 83 | , Pill.styles 84 | , Row.styles 85 | , Select.styles 86 | , Slider.styles 87 | , StatusSlat.styles 88 | , Tab.styles 89 | , Table.styles 90 | , Text.styles 91 | , Text.styles 92 | , Textarea.styles 93 | , Toast.styles 94 | , Tooltip.styles 95 | , Upload.styles 96 | ] 97 | 98 | globals :: JSS 99 | globals = jss 100 | { "@global": 101 | { "*.lumi": 102 | { boxSizing: "border-box" 103 | , margin: "0" 104 | } 105 | , "react-basic-ref": 106 | { display: "flex" 107 | , flexDirection: "column" 108 | , minHeight: "0" 109 | , minWidth: "0" 110 | } 111 | , "body": 112 | { color: cssStringHSLA colors.black 113 | , backgroundColor: cssStringHSLA colors.white 114 | , fontFeatureSettings: "tnum" 115 | , textRendering: "optimizeLegibility" 116 | , "WebkitFontSmoothing": "antialiased" 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Lumi/Components/Svg.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Svg where 2 | 3 | import Prelude 4 | 5 | import Color (cssStringHSLA) 6 | import Lumi.Components.Color (colors) 7 | import React.Basic.Classic (JSX) 8 | import React.Basic.DOM.SVG as RS 9 | 10 | placeholderSvg :: JSX 11 | placeholderSvg = 12 | RS.svg 13 | { viewBox: "0 0 24 24" 14 | , fill: "none" 15 | , stroke: cssStringHSLA colors.black4 16 | , xmlns: "http://www.w3.org/2000/svg" 17 | , children: 18 | [ "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" 19 | ] 20 | <#> 21 | { d: _ 22 | , strokeLinecap: "round" 23 | , strokeLinejoin: "round" 24 | } 25 | >>> RS.path 26 | } 27 | 28 | clientSvg :: JSX 29 | clientSvg = 30 | RS.svg 31 | { xmlns: "http://www.w3.org/2000/svg" 32 | , width: "100%" 33 | , viewBox: "0 0 200 200" 34 | , children: 35 | [ RS.g 36 | { fill 37 | , fillRule 38 | , children: 39 | [ RS.path { d } 40 | , RS.path 41 | { d: "M32.939 71.563l10.19-2.73 35.199 131.365H67.356z" 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | where 48 | fill = "#DDDDDC" 49 | fillRule = "evenodd" 50 | d = "M147.61 55.396a2.89 2.89 0 0 0-2.458.186c-.15.085-15.444 8.745-28.157 12.151-4.362 1.17-7.85 1.576-10.364 1.205-3-.44-3.763-1.892-4.09-3.11-.95-3.546-4.434-8.997-17.082-5.608C71.61 63.93 53.366 76.076 52.593 76.59c-.977.656-1.435 1.829-1.141 2.928l16.669 62.21c.225.84.862 1.519 1.702 1.81a2.85 2.85 0 0 0 2.534-.328c.182-.116 18.226-12.13 31.16-15.595 9.158-2.454 10.03.796 10.315 1.864 1.303 4.863 6.098 9.785 21.532 5.65l.006-.002c13.374-3.584 28.609-12.122 29.248-12.488 1.098-.616 1.64-1.862 1.325-3.039l-16.728-62.427a2.652 2.652 0 0 0-1.605-1.776" 51 | 52 | userSvg :: JSX 53 | userSvg = 54 | RS.svg 55 | { xmlns: "http://www.w3.org/2000/svg" 56 | , width: "100%" 57 | , viewBox: "0 0 200 200" 58 | , children: 59 | [ RS.g 60 | { fill 61 | , fillRule 62 | , children: 63 | [ RS.path { d } 64 | ] 65 | } 66 | ] 67 | } 68 | where 69 | fill = "#DDDDDC" 70 | fillRule = "nonzero" 71 | d = "M30.613 200c4.223-39.011 29.994-69 69.387-69s65.164 29.989 69.387 69H30.613zM100 120c30.281 0 39-17.909 38-40s-12.31-40-38-40c-25.69 0-37 17.909-38 40s7.719 40 38 40z" 72 | -------------------------------------------------------------------------------- /src/Lumi/Components/Tab.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const urlParts = function (href) { 4 | var base = href; 5 | var path = ""; 6 | var query = ""; 7 | var hash = { path: "", query: "" }; 8 | try { 9 | var a = document.createElement("a"); 10 | a.href = href; 11 | base = a.protocol + "//" + a.host; 12 | path = a.pathname; 13 | query = a.search; 14 | var hashParts = a.hash.split("?"); 15 | hash.path = hashParts[0] || ""; 16 | hash.query = hashParts[1] != null ? "?" + hashParts[1] : ""; 17 | } catch (err) { 18 | console.warn(err); 19 | } 20 | return { 21 | base: base, 22 | path: path, 23 | query: query, 24 | hash: hash 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/Lumi/Components/Table.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // This is here until we can get rid of Immutable 4 | export const dataSize = function (data) { 5 | return data.size ? data.size : data.length; 6 | }; 7 | 8 | export const getMouseEventPositionWithOffset = function (domNode, e) { 9 | var bounds = domNode.getBoundingClientRect(); 10 | return { 11 | x: e.pageX - window.scrollX - bounds.left, 12 | y: e.pageY - window.scrollY - bounds.top 13 | }; 14 | }; 15 | 16 | export const checkIsEventTargetInTree = function (domNode, e) { 17 | return e.target === domNode || domNode.contains(e.target); 18 | }; 19 | 20 | export const isRightClick = function (e) { 21 | return e.button === 2; 22 | }; 23 | 24 | export const hasWindowSelection = function () { 25 | return window.getSelection().type === "Range"; 26 | }; 27 | -------------------------------------------------------------------------------- /src/Lumi/Components/Textarea.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.Textarea where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Data.Nullable (Nullable, toNullable) 7 | import Effect.Uncurried (mkEffectFn1) 8 | import Foreign.Object (fromHomogeneous) 9 | import JSS (JSS, jss) 10 | import Lumi.Components.Input (lumiInputDisabledStyles, lumiInputFocusInvalidStyles, lumiInputFocusStyles, lumiInputHoverStyles, lumiInputInvalidStyles, lumiInputPlaceholderStyles, lumiInputStyles) 11 | import React.Basic.Classic (Component, JSX, createComponent, makeStateless) 12 | import React.Basic.DOM (CSS, css) 13 | import React.Basic.DOM as R 14 | import React.Basic.Events (EventHandler) 15 | import Unsafe.Coerce (unsafeCoerce) 16 | 17 | type TextareaProps 18 | = { autoComplete :: String 19 | , disabled :: Boolean 20 | , lines :: Int 21 | , maxLength :: Nullable Int 22 | , minLength :: Nullable Int 23 | , name :: String 24 | , onBlur :: Nullable EventHandler 25 | , onChange :: EventHandler 26 | , onFocus :: Nullable EventHandler 27 | , placeholder :: String 28 | , readOnly :: Boolean 29 | , required :: Boolean 30 | , resizable :: Boolean 31 | , style :: CSS 32 | , testId :: Nullable String 33 | , value :: String 34 | } 35 | 36 | component :: Component TextareaProps 37 | component = createComponent "Textarea" 38 | 39 | textarea :: TextareaProps -> JSX 40 | textarea = makeStateless component render 41 | where 42 | render props = 43 | R.textarea 44 | { _data: fromHomogeneous 45 | { testid: unsafeNullable props.testId 46 | , "disable-resize": show (not props.resizable) 47 | } 48 | , autoComplete: props.autoComplete 49 | , className: "lumi" 50 | , disabled: props.disabled 51 | , maxLength: unsafeNullable props.maxLength 52 | , minLength: unsafeNullable props.minLength 53 | , name: props.name 54 | , onBlur: unsafeNullable props.onBlur 55 | , onChange: props.onChange 56 | , onFocus: unsafeNullable props.onFocus 57 | , placeholder: props.placeholder 58 | , required: props.required 59 | , readOnly: props.readOnly 60 | , rows: props.lines 61 | , style: props.style 62 | , value: props.value 63 | } 64 | 65 | unsafeNullable :: forall a. Nullable a -> a 66 | unsafeNullable = unsafeCoerce 67 | 68 | defaults :: TextareaProps 69 | defaults = 70 | { autoComplete: "on" 71 | , disabled: false 72 | , lines: 2 73 | , maxLength: toNullable Nothing 74 | , minLength: toNullable Nothing 75 | , name: "" 76 | , onBlur: toNullable Nothing 77 | , onChange: mkEffectFn1 $ pure <<< const unit 78 | , onFocus: toNullable Nothing 79 | , placeholder: "" 80 | , required: false 81 | , readOnly: false 82 | , resizable: true 83 | , style: css {} 84 | , testId: toNullable Nothing 85 | , value: "" 86 | } 87 | 88 | styles :: JSS 89 | styles = 90 | jss 91 | { "@global": 92 | { "textarea.lumi": 93 | { fontFamily: "inherit" 94 | , fontSize: "14px" 95 | , lineHeight: "20px" 96 | , touchAction: "manipulation" 97 | , boxSizing: "border-box" 98 | , resize: "vertical" 99 | , extend: lumiInputStyles 100 | , minHeight: "40px" 101 | , height: "unset" 102 | , "@media (min-width: 860px)": 103 | { minHeight: "32px" 104 | , height: "unset" 105 | } 106 | , "&:hover": lumiInputHoverStyles 107 | , "&:invalid": lumiInputInvalidStyles 108 | , "&:focus": 109 | { extend: lumiInputFocusStyles 110 | , "&:invalid": lumiInputFocusInvalidStyles 111 | } 112 | , "&:disabled": lumiInputDisabledStyles 113 | , "&::-webkit-input-placeholder": lumiInputPlaceholderStyles 114 | , "&::-moz-placeholder": lumiInputPlaceholderStyles 115 | , "&:-ms-input-placeholder": lumiInputPlaceholderStyles 116 | , "&::placeholder": lumiInputPlaceholderStyles 117 | , "&[data-disable-resize=\"true\"]": { resize: "none" } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Lumi/Components/Upload.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const createObjectURL = function (file) { 4 | return URL.createObjectURL(file); 5 | }; 6 | 7 | export const xhrUpload = function (uri, file, onProgress, onError, onComplete) { 8 | var xhr = new XMLHttpRequest(); 9 | 10 | xhr.upload.addEventListener( 11 | "progress", 12 | function (e) { 13 | onProgress({ totalBytes: e.total | 0, uploadedBytes: e.loaded | 0 }); 14 | }, 15 | { passive: true } 16 | ); 17 | 18 | xhr.addEventListener( 19 | "load", 20 | function (e) { 21 | onComplete(e.responseText); 22 | }, 23 | { passive: true } 24 | ); 25 | 26 | xhr.addEventListener( 27 | "error", 28 | function (e) { 29 | onError(e.error); 30 | }, 31 | { passive: true } 32 | ); 33 | 34 | xhr.open("post", uri, true); 35 | xhr.setRequestHeader("Content-Type", "multipart/form-data"); 36 | xhr.send(file); 37 | }; 38 | -------------------------------------------------------------------------------- /src/Lumi/Components/ZIndex.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components.ZIndex where 2 | 3 | ziButtonGroup :: Int 4 | ziButtonGroup = 1000 5 | 6 | ziDropdownButton :: Int 7 | ziDropdownButton = 1000 8 | 9 | ziInputGroup :: Int 10 | ziInputGroup = 1000 11 | 12 | ziModal :: Int 13 | ziModal = 10000 14 | 15 | ziNavigationBar :: Int 16 | ziNavigationBar = 100 17 | 18 | ziNavigationDropdown :: Int 19 | ziNavigationDropdown = 101 20 | 21 | ziSelect :: Int 22 | ziSelect = 1000 23 | 24 | ziTableHeader :: Int 25 | ziTableHeader = 12 26 | 27 | ziTableHeaderMenu :: Int 28 | ziTableHeaderMenu = 14 29 | 30 | ziTableLockedColumn :: Int 31 | ziTableLockedColumn = 11 32 | 33 | ziTableLockedColumnHeader :: Int 34 | ziTableLockedColumnHeader = 13 35 | 36 | ziToast :: Int 37 | ziToast = 100000 38 | 39 | ziTooltip :: Int 40 | ziTooltip = 1000000 41 | -------------------------------------------------------------------------------- /src/Lumi/Components2/Box.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components2.Box where 2 | 3 | import Prelude 4 | 5 | import Effect.Unsafe (unsafePerformEffect) 6 | import Lumi.Components (LumiComponent, lumiComponent) 7 | import Lumi.Styles (toCSS) 8 | import Lumi.Styles.Box as Styles.Box 9 | import Lumi.Styles.Theme (useTheme) 10 | import React.Basic.Classic (JSX) 11 | import React.Basic.DOM as R 12 | import React.Basic.Emotion as E 13 | import React.Basic.Hooks as React 14 | 15 | type BoxProps 16 | = ( content :: Array JSX ) 17 | 18 | box :: LumiComponent BoxProps 19 | box = 20 | unsafePerformEffect do 21 | lumiComponent "Box" { content: [] } \props -> React.do 22 | theme <- useTheme 23 | pure 24 | $ E.element R.div' 25 | { children: props.content 26 | , className: props.className 27 | , css: theme # toCSS Styles.Box.box <> props.css 28 | } 29 | 30 | row :: LumiComponent BoxProps 31 | row = box <<< Styles.Box._row 32 | 33 | column :: LumiComponent BoxProps 34 | column = box <<< Styles.Box._column 35 | -------------------------------------------------------------------------------- /src/Lumi/Components2/ButtonGroup.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components2.ButtonGroup where 2 | 3 | import Prelude 4 | 5 | import Effect.Unsafe (unsafePerformEffect) 6 | import Lumi.Components (PropsModifier) 7 | import Lumi.Components as L 8 | import Lumi.Components.ZIndex (ziButtonGroup) 9 | import Lumi.Styles (css, nested, px, str, style_, toCSS) 10 | import Lumi.Styles.Box (_row, box) 11 | import Lumi.Styles.Theme (useTheme) 12 | import React.Basic.DOM as R 13 | import React.Basic.Emotion as E 14 | import React.Basic.Hooks (JSX) 15 | import React.Basic.Hooks as React 16 | 17 | type ButtonGroupProps 18 | = ( component :: ButtonGroup 19 | , content :: Array JSX 20 | ) 21 | 22 | buttonGroup :: L.LumiComponent ButtonGroupProps 23 | buttonGroup = 24 | unsafePerformEffect do 25 | L.lumiComponent "ButtonGroup" { component: ButtonGroup, content: [] } \props -> React.do 26 | theme <- useTheme 27 | pure 28 | $ E.element R.div' 29 | { className: props.className 30 | , children: props.content 31 | , css: theme # toCSS styles <> props.css 32 | } 33 | 34 | where 35 | styles = 36 | box 37 | <<< _row 38 | <<< style_ 39 | ( css 40 | { label: str "buttonGroup" 41 | , "& > *:not(:last-child)": 42 | nested 43 | $ css 44 | { marginRight: px 8 45 | } 46 | } 47 | ) 48 | 49 | 50 | data ButtonGroup = ButtonGroup 51 | 52 | type ButtonGroupModifier = forall r. PropsModifier ( component :: ButtonGroup | r ) 53 | 54 | joined :: ButtonGroupModifier 55 | joined = 56 | style_ 57 | $ css 58 | { label: str "joined" 59 | , "& > *:not(:last-child)": 60 | nested 61 | $ css 62 | { marginRight: px (-1) 63 | , borderTopRightRadius: px 0 64 | , borderBottomRightRadius: px 0 65 | } 66 | , "& > *:not(:first-child)": 67 | nested 68 | $ css 69 | { borderTopLeftRadius: px 0 70 | , borderBottomLeftRadius: px 0 71 | } 72 | , "& > *:focus, & > *:hover": 73 | nested 74 | $ css 75 | { zIndex: px ziButtonGroup 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Lumi/Components2/Clip.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const selectNodeContents = node => { 4 | const range = document.createRange(); 5 | range.selectNodeContents(node); 6 | var sel = window.getSelection(); 7 | sel.removeAllRanges(); 8 | sel.addRange(range); 9 | return sel; 10 | }; 11 | 12 | export const copyNodeContents = (success, failure, node) => { 13 | try { 14 | const sel = selectNodeContents(node); 15 | if (window.navigator.clipboard != null) { 16 | window.navigator.clipboard 17 | .writeText(sel.toString().trim()) 18 | .then(() => { 19 | sel.removeAllRanges(); 20 | }) 21 | .then(success, failure); 22 | return; 23 | } else { 24 | const copyResult = window.document.execCommand("copy"); 25 | if (copyResult) { 26 | sel.removeAllRanges(); 27 | return success(); 28 | } else { 29 | return failure(new Error("Failed to copy")); 30 | } 31 | } 32 | } catch (e) { 33 | failure(e); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/Lumi/Components2/Clip.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components2.Clip where 2 | 3 | import Prelude 4 | 5 | import Data.Either (Either(..)) 6 | import Data.Foldable (for_) 7 | import Data.Nullable (Nullable) 8 | import Data.Nullable as Nullable 9 | import Effect (Effect) 10 | import Effect.Aff (Aff, Error, Milliseconds(..), delay, makeAff, message, nonCanceler) 11 | import Effect.Class (liftEffect) 12 | import Effect.Console as Console 13 | import Effect.Uncurried (EffectFn1, EffectFn3, mkEffectFn1, runEffectFn3) 14 | import Effect.Unsafe (unsafePerformEffect) 15 | import Lumi.Components (LumiComponent, lumiComponent, ($$$)) 16 | import Lumi.Components.Spacing (Space(..), hspace) 17 | import Lumi.Components2.Box (box) 18 | import Lumi.Components2.Button (linkButton, loadingContent, onPress) 19 | import Lumi.Styles (toCSS) 20 | import Lumi.Styles as S 21 | import Lumi.Styles.Box (FlexAlign(..), _align, _flex) 22 | import Lumi.Styles.Box as Styles.Box 23 | import Lumi.Styles.Clip as Styles.Clip 24 | import Lumi.Styles.Theme (LumiTheme(..), useTheme) 25 | import React.Basic.DOM as R 26 | import React.Basic.Emotion as E 27 | import React.Basic.Hooks (JSX, Ref, readRefMaybe, useRef) 28 | import React.Basic.Hooks as React 29 | import Web.DOM (Node) 30 | 31 | type ClipProps 32 | = ( content :: Array JSX 33 | ) 34 | 35 | clip :: LumiComponent ClipProps 36 | clip = 37 | unsafePerformEffect do 38 | lumiComponent "Clip" defaults \props -> React.do 39 | theme@(LumiTheme { colors }) <- useTheme 40 | ref <- useRef Nullable.null 41 | let 42 | copyButton = 43 | linkButton 44 | $ S.style_ (S.css { "&:disabled": S.nested $ S.css { color: S.color colors.black1 } }) 45 | $ onPress do 46 | copy ref 47 | delay $ Milliseconds 5000.0 48 | $ loadingContent [ box $ _flex $ _align End $$$ [ R.text "Copied!" ] ] 49 | $$$ [ box $ _flex $ _align End $$$ [ R.text "Copy" ] ] 50 | pure 51 | $ E.element R.div' 52 | { className: props.className 53 | , css: theme # toCSS Styles.Clip.clip <> props.css 54 | , children: 55 | [ E.element R.div' 56 | { className: "" 57 | , css: theme # toCSS (Styles.Box.box <<< Styles.Box._justify Center) 58 | , ref 59 | , children: props.content 60 | } 61 | , hspace S8 62 | , box 63 | $ _align End 64 | $$$ [ copyButton ] 65 | ] 66 | } 67 | where 68 | defaults = { content: [] } 69 | 70 | copy :: Ref (Nullable Node) -> Aff Unit 71 | copy nodeRef = do 72 | nodeM <- liftEffect do readRefMaybe nodeRef 73 | for_ nodeM \node -> 74 | makeAff \done -> do 75 | runEffectFn3 copyNodeContents 76 | ( done $ Right unit ) 77 | ( mkEffectFn1 \e -> do 78 | Console.error $ message e 79 | done $ Right unit 80 | ) 81 | node 82 | pure nonCanceler 83 | 84 | foreign import copyNodeContents :: EffectFn3 (Effect Unit) (EffectFn1 Error Unit) Node Unit 85 | -------------------------------------------------------------------------------- /src/Lumi/Components2/Link.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components2.Link where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Data.Newtype (un) 7 | import Effect (Effect) 8 | import Effect.Uncurried (runEffectFn1) 9 | import Effect.Unsafe (unsafePerformEffect) 10 | import Foreign.Object (fromHomogeneous) 11 | import Lumi.Components (LumiComponent, lumiComponent, unsafeMaybeToNullableAttr) 12 | import Lumi.Styles (toCSS) 13 | import Lumi.Styles.Link as Styles.Link 14 | import Lumi.Styles.Theme (useTheme) 15 | import React.Basic.Classic (JSX) 16 | import React.Basic.DOM as R 17 | import React.Basic.DOM.Events (altKey, button, ctrlKey, metaKey, preventDefault, shiftKey, stopPropagation) 18 | import React.Basic.Emotion as E 19 | import React.Basic.Events (handler, merge, syntheticEvent) 20 | import React.Basic.Hooks as React 21 | import Web.HTML.History (URL(..)) 22 | 23 | type LinkProps 24 | = ( href :: URL 25 | , navigate :: Maybe (Effect Unit) 26 | , tabIndex :: Maybe Int 27 | , target :: Maybe String 28 | , rel :: Maybe String 29 | , download :: Maybe String 30 | , ariaLabel :: Maybe String 31 | , content :: Array JSX 32 | , className :: String 33 | ) 34 | 35 | link :: LumiComponent LinkProps 36 | link = 37 | unsafePerformEffect do 38 | lumiComponent "Link" defaults \props@{ className } -> React.do 39 | theme <- useTheme 40 | pure 41 | $ E.element R.a' 42 | { _aria: unsafeMaybeToNullableAttr $ map (fromHomogeneous <<< { label: _ }) props.ariaLabel 43 | , css: theme # toCSS Styles.Link.link <> props.css 44 | , children: props.content 45 | , className 46 | , href: un URL props.href 47 | , onClick: 48 | handler (merge { button, metaKey, altKey, ctrlKey, shiftKey, syntheticEvent }) \{ button, metaKey, altKey, ctrlKey, shiftKey, syntheticEvent } -> do 49 | case props.navigate, button, metaKey, altKey, ctrlKey, shiftKey of 50 | Just n', Just 0, Just false, Just false, Just false, Just false -> 51 | runEffectFn1 52 | (handler (stopPropagation <<< preventDefault) $ const n') 53 | syntheticEvent 54 | _, _, _, _, _, _ -> 55 | runEffectFn1 56 | (handler stopPropagation mempty) 57 | syntheticEvent 58 | , target: unsafeMaybeToNullableAttr props.target 59 | , rel: unsafeMaybeToNullableAttr props.rel 60 | , tabIndex: unsafeMaybeToNullableAttr props.tabIndex 61 | , download: unsafeMaybeToNullableAttr props.download 62 | } 63 | where 64 | defaults = 65 | { className: "" 66 | , href: URL "" 67 | , navigate: Nothing 68 | , tabIndex: Nothing 69 | , target: Nothing 70 | , rel: Nothing 71 | , download: Nothing 72 | , ariaLabel: Nothing 73 | , content: [] 74 | } 75 | -------------------------------------------------------------------------------- /src/Lumi/Components2/PasswordStrength.js: -------------------------------------------------------------------------------- 1 | export {default as passwordStrengthBar} from 'react-password-strength-bar'; -------------------------------------------------------------------------------- /src/Lumi/Components2/PasswordStrength.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components2.PasswordStrength where 2 | 3 | 4 | import React.Basic.Hooks as Hooks 5 | 6 | type PasswordStrengthProps = 7 | { password :: String 8 | , minLength :: Int 9 | } 10 | 11 | foreign import passwordStrengthBar :: Hooks.ReactComponent PasswordStrengthProps -------------------------------------------------------------------------------- /src/Lumi/Components2/QRCode.js: -------------------------------------------------------------------------------- 1 | export {default as qrcode_} from "qrcode.react"; 2 | 3 | export const generateSVGUrl = ref => () => { 4 | const containerNode = ref.current; 5 | if (containerNode == null) { 6 | throw new Error("Cannot save the contents of an empty ref"); 7 | } 8 | const svgNode = containerNode.querySelector("svg"); 9 | if (svgNode == null) { 10 | throw new Error("Inner SVG node not found"); 11 | } 12 | 13 | const data = new XMLSerializer().serializeToString(svgNode); 14 | const svg = new Blob([data], { type: "image/svg+xml;charset=utf-8" }); 15 | const url = URL.createObjectURL(svg); 16 | const dispose = () => URL.revokeObjectURL(url); 17 | return { url, dispose }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/Lumi/Components2/QRCode.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components2.QRCode 2 | ( useQRCode 3 | , UseQRCode 4 | , ErrorCorrectLevel(..) 5 | , errorCorrectLevelToString 6 | , qrcode_ 7 | , generateSVGUrl 8 | ) where 9 | 10 | import Prelude 11 | 12 | import Data.Maybe (Maybe(..)) 13 | import Data.Newtype (class Newtype) 14 | import Data.Nullable as Nullable 15 | import Effect (Effect) 16 | import Effect.Unsafe (unsafePerformEffect) 17 | import Lumi.Components (LumiComponent, lumiComponent) 18 | import Lumi.Styles (StyleModifier, style_, toCSS) 19 | import Lumi.Styles.Box (box) 20 | import Lumi.Styles.Theme (useTheme) 21 | import React.Basic.DOM as R 22 | import React.Basic.Emotion as E 23 | import React.Basic.Hooks (type (/\), Hook, ReactComponent, Ref, UnsafeReference(..), UseEffect, UseMemo, UseRef, UseState, coerceHook, element, useEffect, useMemo, useRef, useState, (/\)) 24 | import React.Basic.Hooks as React 25 | import Web.DOM (Node) 26 | import Web.HTML.History (URL(..)) 27 | 28 | useQRCode :: ErrorCorrectLevel -> String -> Hook UseQRCode { qrcode :: LumiComponent (), url :: Maybe URL } 29 | useQRCode level value = 30 | coerceHook React.do 31 | ref <- useRef Nullable.null 32 | qrcode <- 33 | useMemo (value /\ level) \_ -> 34 | unsafePerformEffect do 35 | lumiComponent "QRCode" {} \props -> React.do 36 | theme <- useTheme 37 | pure 38 | $ E.element R.div' 39 | { children: 40 | [ element qrcode_ 41 | { value 42 | , level: errorCorrectLevelToString level 43 | , renderAs: "svg" 44 | , xmlns: "http://www.w3.org/2000/svg" 45 | , size: Nullable.null 46 | , bgColor: "rgba(255,255,255,0.0)" 47 | , fgColor: "rgba(0,0,0,1.0)" 48 | } 49 | ] 50 | , ref 51 | , className: props.className 52 | , css: theme # toCSS qrcodeStyle <> props.css 53 | } 54 | url /\ setUrl <- useState Nothing 55 | useEffect (UnsafeReference qrcode) do 56 | svgUrl <- generateSVGUrl ref 57 | setUrl \_ -> Just $ URL svgUrl.url 58 | pure svgUrl.dispose 59 | pure { qrcode, url } 60 | 61 | qrcodeStyle :: StyleModifier 62 | qrcodeStyle = box <<< style_ (E.css { label: E.str "qrcode" }) 63 | 64 | newtype UseQRCode hooks 65 | = UseQRCode 66 | ( UseEffect 67 | ( UnsafeReference (LumiComponent ()) ) 68 | ( UseState 69 | ( Maybe URL ) 70 | ( UseMemo 71 | (String /\ ErrorCorrectLevel) 72 | (LumiComponent ()) 73 | (UseRef (Nullable.Nullable Node) hooks) 74 | ) 75 | ) 76 | ) 77 | 78 | derive instance ntUseQRCode :: Newtype (UseQRCode hooks) _ 79 | 80 | data ErrorCorrectLevel 81 | = ECLLow 82 | | ECLMedium 83 | | ECLQuartile 84 | | ECLHigh 85 | 86 | derive instance eqErrorCorrectLevel :: Eq ErrorCorrectLevel 87 | 88 | errorCorrectLevelToString :: ErrorCorrectLevel -> String 89 | errorCorrectLevelToString = case _ of 90 | ECLLow -> "L" 91 | ECLMedium -> "M" 92 | ECLQuartile -> "Q" 93 | ECLHigh -> "H" 94 | 95 | foreign import qrcode_ :: 96 | ReactComponent 97 | { value :: String 98 | , level :: String 99 | , renderAs :: String 100 | , xmlns :: String 101 | , size :: Nullable.Nullable Int 102 | , bgColor :: String 103 | , fgColor :: String 104 | } 105 | 106 | foreign import generateSVGUrl :: Ref (Nullable.Nullable Node) -> Effect { url :: String, dispose :: Effect Unit } 107 | -------------------------------------------------------------------------------- /src/Lumi/Components2/ScrollObserver.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Walks up the DOM tree starting at the given node to find 4 | // the first parent with a scroll bar (including auto, which 5 | // on some devices hides the scroll bar until hovered). 6 | export const getScrollParent = node => () => { 7 | if (!(node instanceof HTMLElement)) { 8 | return document.body; 9 | } 10 | 11 | const computedStyle = window.getComputedStyle(node); 12 | const overflowY = computedStyle && computedStyle.overflowY; 13 | const overflowX = computedStyle && computedStyle.overflowX; 14 | const isScrollable = 15 | (overflowY && 16 | !(overflowY.includes("visible") || overflowY.includes("hidden"))) || 17 | (overflowX && 18 | !(overflowX.includes("visible") || overflowX.includes("hidden"))); 19 | 20 | if (isScrollable && node.scrollHeight >= node.clientHeight) { 21 | return node; 22 | } else { 23 | return getScrollParent(node.parentNode)(); 24 | } 25 | }; 26 | 27 | export const addPassiveEventListener = type => listener => capture => target => () => 28 | target.addEventListener(type, listener, { passive: true, capture }); 29 | 30 | export const removePassiveEventListener = type => listener => capture => target => () => 31 | target.removeEventListener(type, listener, { passive: true, capture }); 32 | -------------------------------------------------------------------------------- /src/Lumi/Components2/ScrollObserver.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components2.ScrollObserver where 2 | 3 | import Prelude 4 | import Data.Newtype (class Newtype) 5 | import Data.Nullable (Nullable) 6 | import Data.Nullable as Nullable 7 | import Effect (Effect) 8 | import Effect.Unsafe (unsafePerformEffect) 9 | import Lumi.Components (LumiComponent, lumiComponentFromHook) 10 | import React.Basic.Classic (JSX) 11 | import React.Basic.Hooks (Hook, UnsafeReference(..), UseEffect, UseState, coerceHook, useEffect, useState, (/\)) 12 | import React.Basic.Hooks as React 13 | import Web.DOM (Element, Node) 14 | import Web.DOM.Element (scrollLeft, scrollTop) 15 | import Web.DOM.Element as Element 16 | import Web.Event.Event (EventType(..)) 17 | import Web.Event.EventTarget (EventListener, EventTarget, eventListener) 18 | 19 | newtype UseScrollObserver hooks 20 | = UseScrollObserver (UseEffect (UnsafeReference (Nullable Node)) (UseState Boolean (UseState Boolean hooks))) 21 | 22 | derive instance ntUseScrollObserver :: Newtype (UseScrollObserver hooks) _ 23 | 24 | useScrollObserver :: Nullable Node -> Hook UseScrollObserver { hasScrolledX :: Boolean, hasScrolledY :: Boolean } 25 | useScrollObserver root = 26 | coerceHook React.do 27 | hasScrolledY /\ setHasScrolledY <- useState false 28 | hasScrolledX /\ setHasScrolledX <- useState false 29 | useEffect (UnsafeReference root) do 30 | scrollParent <- getScrollParent root 31 | let 32 | onScroll = do 33 | top <- scrollTop scrollParent 34 | left <- scrollLeft scrollParent 35 | setHasScrolledY \_ -> top > 0.0 36 | setHasScrolledX \_ -> left > 0.0 37 | onScrollListener <- eventListener \_ -> onScroll 38 | onScroll 39 | Element.toEventTarget scrollParent # addPassiveEventListener (EventType "scroll") onScrollListener false 40 | pure do 41 | Element.toEventTarget scrollParent # removePassiveEventListener (EventType "scroll") onScrollListener false 42 | pure { hasScrolledY, hasScrolledX } 43 | 44 | type ScrollObserverProps 45 | = ( node :: Nullable Node 46 | , render :: { hasScrolledX :: Boolean, hasScrolledY :: Boolean } -> JSX 47 | ) 48 | 49 | scrollObserver :: LumiComponent ScrollObserverProps 50 | scrollObserver = 51 | unsafePerformEffect do 52 | lumiComponentFromHook "ScrollObserver" defaults (useScrollObserver <<< _.node) 53 | where 54 | defaults = { node: Nullable.null, render: \_ -> mempty } 55 | 56 | foreign import getScrollParent :: Nullable Node -> Effect Element 57 | 58 | -- | Adds a listener to an event target. The boolean argument indicates whether 59 | -- | the listener should be added for the "capture" phase. 60 | foreign import addPassiveEventListener :: 61 | EventType -> 62 | EventListener -> 63 | Boolean -> 64 | EventTarget -> 65 | Effect Unit 66 | 67 | -- | Removes a listener to an event target. The boolean argument indicates 68 | -- | whether the listener should be removed for the "capture" phase. 69 | foreign import removePassiveEventListener :: 70 | EventType -> 71 | EventListener -> 72 | Boolean -> 73 | EventTarget -> 74 | Effect Unit 75 | -------------------------------------------------------------------------------- /src/Lumi/Components2/Slat.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Components2.Slat 2 | ( SlatProps 3 | , SlatInteraction 4 | , slat 5 | , _interactive 6 | , _interactiveBackground 7 | , module Styles.Slat 8 | ) where 9 | 10 | import Prelude 11 | import Data.Maybe (Maybe(..)) 12 | import Data.Newtype (un) 13 | import Effect (Effect) 14 | import Effect.Unsafe (unsafePerformEffect) 15 | import Lumi.Components (LumiComponent, PropsModifier, lumiComponent, propsModifier) 16 | import Lumi.Styles (style, style_, toCSS) 17 | import Lumi.Styles.Slat (_interactive, slat) as Styles.Slat.Hidden 18 | import Lumi.Styles.Slat hiding (_interactive,slat) as Styles.Slat 19 | import Lumi.Styles.Theme (LumiTheme(..), useTheme) 20 | import React.Basic.DOM as R 21 | import React.Basic.DOM.Events (capture_) 22 | import React.Basic.Emotion as E 23 | import React.Basic.Hooks (JSX) 24 | import React.Basic.Hooks as React 25 | import Web.HTML.History (URL(..)) 26 | 27 | type SlatProps 28 | = ( content :: Array JSX 29 | , interaction :: Maybe SlatInteraction 30 | ) 31 | 32 | type SlatInteraction 33 | = { onClick :: Effect Unit 34 | , tabIndex :: Int 35 | , href :: Maybe URL 36 | } 37 | 38 | slat :: LumiComponent SlatProps 39 | slat = 40 | unsafePerformEffect do 41 | lumiComponent "Slat" defaults \props@{ className } -> React.do 42 | theme <- useTheme 43 | pure case props.interaction of 44 | Nothing -> 45 | E.element R.div' 46 | { css: theme # toCSS slatStyle <> props.css 47 | , children: props.content 48 | , className 49 | } 50 | Just interaction@{ href: Nothing } -> 51 | E.element R.button' 52 | { css: theme # toCSS slatStyleInteractive <> props.css 53 | , children: props.content 54 | , onClick: capture_ interaction.onClick 55 | , tabIndex: interaction.tabIndex 56 | , className 57 | } 58 | Just interaction@{ href: Just href } -> 59 | E.element R.a' 60 | { css: theme # toCSS slatStyleInteractive <> props.css 61 | , children: props.content 62 | , onClick: capture_ interaction.onClick 63 | , tabIndex: interaction.tabIndex 64 | , href: un URL href 65 | , className 66 | } 67 | where 68 | defaults = 69 | { content: [] 70 | , interaction: Nothing 71 | } 72 | 73 | slatStyle = 74 | Styles.Slat.Hidden.slat 75 | <<< style_ (E.css { appearance: E.none }) 76 | 77 | slatStyleInteractive = 78 | slatStyle 79 | <<< Styles.Slat.Hidden._interactive 80 | 81 | _interactive :: SlatInteraction -> PropsModifier SlatProps 82 | _interactive interaction = 83 | propsModifier 84 | _ 85 | { interaction = Just interaction 86 | } 87 | 88 | _interactiveBackground :: SlatInteraction -> PropsModifier SlatProps 89 | _interactiveBackground interaction = 90 | propsModifier 91 | _ 92 | { interaction = Just interaction 93 | } 94 | <<< style \(LumiTheme theme) -> 95 | E.css 96 | { "&:hover": 97 | E.nested 98 | $ E.css 99 | { backgroundColor: E.color theme.colors.primary4 100 | , borderColor: E.color theme.colors.black4 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Lumi/Styles.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Styles 2 | ( module Lumi.Styles 3 | , module Emotion 4 | ) where 5 | 6 | import Prelude 7 | 8 | import Data.Foldable (fold) 9 | import Lumi.Components (PropsModifier, propsModifier) 10 | import Lumi.Styles.Theme (LumiTheme) 11 | import React.Basic.Emotion hiding (element,style) as Emotion 12 | 13 | type StyleModifier = forall props. PropsModifier props 14 | 15 | -- | Lift a themed set of styles into a `StyleModifier` for composition with other modifiers. 16 | style :: (LumiTheme -> Emotion.Style) -> StyleModifier 17 | style f = propsModifier \props -> props { css = props.css <> f } 18 | 19 | -- | Lift a static set of styles into a `StyleModifier` for composition with other modifiers. 20 | style_ :: Emotion.Style -> StyleModifier 21 | style_ = style <<< const 22 | 23 | -- | Lift an array of themed styles into a `StyleModifier` for composition with other modifiers. 24 | styles :: Array (LumiTheme -> Emotion.Style) -> StyleModifier 25 | styles = style <<< fold 26 | 27 | -- | Lift an array of static styles into a `StyleModifier` for composition with other modifiers. 28 | styles_ :: Array Emotion.Style -> StyleModifier 29 | styles_ = style_ <<< fold 30 | 31 | -- | Flatten a `PropsModifier` and extract the Emotion styles for use with `React.Basic.Emotion.element`. 32 | -- | This function is mainly used inside component implementations where the `LumiComponent` boundary 33 | -- | gives way to DOM components or other `ReactComponent`s. 34 | toCSS :: 35 | PropsModifier () -> 36 | LumiTheme -> 37 | Emotion.Style 38 | toCSS m = (m identity { className: "", css: mempty }).css 39 | -------------------------------------------------------------------------------- /src/Lumi/Styles/Border.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Styles.Border where 2 | 3 | import Prelude 4 | 5 | import Lumi.Components.Spacing (Space(..)) 6 | import Lumi.Styles (StyleModifier, px2, solid, style, style_) 7 | import Lumi.Styles.Box (box) 8 | import Lumi.Styles.Box as Box 9 | import Lumi.Styles.Theme (LumiTheme(..)) 10 | import React.Basic.Emotion (color, css, px, nested, none, prop, str) 11 | 12 | border :: StyleModifier 13 | border = 14 | box 15 | <<< style \(LumiTheme theme) -> 16 | css 17 | { label: str "border" 18 | , borderWidth: px 1 19 | , borderColor: color theme.colors.black4 20 | , borderStyle: solid 21 | , padding: px2 8 16 22 | } 23 | 24 | _round :: StyleModifier 25 | _round = 26 | style_ 27 | $ css 28 | { borderRadius: px 4 29 | } 30 | 31 | _topBottom :: StyleModifier 32 | _topBottom = 33 | style_ 34 | ( css 35 | { borderLeft: none 36 | , borderRight: none 37 | , borderRadius: px 0 38 | , paddingLeft: px 0 39 | , paddingRight: px 0 40 | } 41 | ) 42 | 43 | _interactive :: StyleModifier 44 | _interactive = 45 | Box._interactive 46 | <<< Box._focusable 47 | <<< style \(LumiTheme theme) -> 48 | css 49 | { "&:hover": 50 | nested 51 | $ css 52 | { borderColor: color theme.colors.black2 53 | } 54 | } 55 | 56 | _listSpaced :: StyleModifier 57 | _listSpaced = 58 | style \(LumiTheme theme) -> 59 | css 60 | { "&:not(:first-child)": 61 | nested 62 | $ css 63 | { marginTop: prop S8 64 | } 65 | } 66 | 67 | _listCompact :: StyleModifier 68 | _listCompact = 69 | style \(LumiTheme theme) -> 70 | css 71 | { "&:first-child": 72 | nested 73 | $ css 74 | { borderTopColor: color theme.colors.transparent 75 | } 76 | , "&:last-child": 77 | nested 78 | $ css 79 | { borderBottomColor: color theme.colors.transparent 80 | } 81 | , "&:not(:first-child)": 82 | nested 83 | $ css 84 | { marginTop: px (-1) 85 | , ":not(:hover)": 86 | nested 87 | $ css 88 | { borderTopColor: color theme.colors.transparent 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Lumi/Styles/Box.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Styles.Box where 2 | 3 | import Prelude 4 | 5 | import Color (cssStringHSLA) 6 | import Lumi.Styles (StyleModifier, auto, baseline, borderBox, center, column, flex, flexEnd, flexStart, manipulation, none, pointer, px, row, spaceAround, spaceBetween, spaceEvenly, stretch, style, style_, wrap) 7 | import Lumi.Styles.Theme (LumiTheme(..)) 8 | import React.Basic.Emotion (class IsStyleProperty, css, nested, prop, str) 9 | 10 | box :: StyleModifier 11 | box = 12 | style_ 13 | $ css 14 | { label: str "box" 15 | , display: flex 16 | , flexDirection: column 17 | , boxSizing: borderBox 18 | , minHeight: px 0 19 | , minWidth: auto 20 | , flex: str "0 0 auto" 21 | , margin: px 0 22 | , padding: px 0 23 | } 24 | 25 | _row :: StyleModifier 26 | _row = 27 | style_ 28 | $ css { flexDirection: row } 29 | 30 | _column :: StyleModifier 31 | _column = 32 | style_ 33 | $ css { flexDirection: column } 34 | 35 | _wrap :: StyleModifier 36 | _wrap = 37 | style_ 38 | $ css { flexWrap: wrap } 39 | 40 | _flex :: StyleModifier 41 | _flex = 42 | style_ 43 | $ css { flex: str "1" } 44 | 45 | data FlexAlign 46 | = Start 47 | | End 48 | | Center 49 | | Stretch 50 | | Baseline 51 | | SpaceAround 52 | | SpaceBetween 53 | | SpaceEvenly 54 | 55 | instance isStylePropertyFlexAlign :: IsStyleProperty FlexAlign where 56 | prop a = 57 | case a of 58 | Start -> flexStart 59 | End -> flexEnd 60 | Center -> center 61 | Stretch -> stretch 62 | Baseline -> baseline 63 | SpaceAround -> spaceAround 64 | SpaceBetween -> spaceBetween 65 | SpaceEvenly -> spaceEvenly 66 | 67 | _justify :: FlexAlign -> StyleModifier 68 | _justify a = style_ $ css { justifyContent: prop a } 69 | 70 | _align :: FlexAlign -> StyleModifier 71 | _align a = style_ $ css { alignItems: prop a } 72 | 73 | _alignSelf :: FlexAlign -> StyleModifier 74 | _alignSelf a = style_ $ css { alignSelf: prop a } 75 | 76 | _interactive :: StyleModifier 77 | _interactive = 78 | style_ 79 | $ css 80 | $ { touchAction: manipulation 81 | , userSelect: none 82 | , cursor: pointer 83 | } 84 | 85 | _focusable :: StyleModifier 86 | _focusable = 87 | style \(LumiTheme theme) -> 88 | css 89 | { "&:focus-within, &:active": 90 | nested 91 | $ css 92 | { outline: str "0" 93 | , boxShadow: str ("0 0 0 3px " <> cssStringHSLA theme.colors.primary3) 94 | } 95 | , "&::-moz-focus-inner": 96 | nested 97 | $ css 98 | { border: str "0" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Lumi/Styles/Clip.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Styles.Clip where 2 | 3 | import Prelude 4 | 5 | import Lumi.Styles (StyleModifier, style) 6 | import Lumi.Styles.Border (_round, border) 7 | import Lumi.Styles.Box (FlexAlign(..), _justify, _row) 8 | import Lumi.Styles.Theme (LumiTheme(..)) 9 | import React.Basic.Emotion (color, css) 10 | 11 | clip :: StyleModifier 12 | clip = 13 | border 14 | <<< _round 15 | <<< _row 16 | <<< _justify SpaceBetween 17 | <<< style \(LumiTheme { colors }) -> 18 | css 19 | { borderColor: color colors.black5 20 | , backgroundColor: color colors.black5 21 | } 22 | -------------------------------------------------------------------------------- /src/Lumi/Styles/Link.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Styles.Link where 2 | 3 | import Prelude 4 | 5 | import Lumi.Styles (StyleModifier, color, nested, none, pointer, style, underline) 6 | import Lumi.Styles.Box (box) 7 | import Lumi.Styles.Theme (LumiTheme(..)) 8 | import React.Basic.Emotion (css) 9 | 10 | link :: StyleModifier 11 | link = 12 | box 13 | <<< style \(LumiTheme theme) -> 14 | css 15 | { color: color theme.colors.primary 16 | , textDecoration: none 17 | , "&:visited": 18 | nested 19 | $ css 20 | { color: color theme.colors.primary 21 | , textDecoration: none 22 | } 23 | , "&:hover": 24 | nested 25 | $ css 26 | { cursor: pointer 27 | , textDecoration: underline 28 | } 29 | } 30 | 31 | secondary :: StyleModifier 32 | secondary = style \(LumiTheme theme) -> 33 | css 34 | { color: color theme.colors.black 35 | , textDecoration: none 36 | , "&:visited": 37 | nested 38 | $ css 39 | { color: color theme.colors.black 40 | , textDecoration: none 41 | } 42 | , "&:hover": 43 | nested 44 | $ css 45 | { cursor: pointer 46 | , textDecoration: underline 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Lumi/Styles/Loader.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Styles.Loader where 2 | 3 | 4 | import Lumi.Styles (Style, StyleModifier, StyleProperty, borderBox, color, css, inlineBlock, keyframes, percent, px, solid, str, style) 5 | import Lumi.Styles.Theme (LumiTheme(..)) 6 | 7 | loader :: StyleModifier 8 | loader = 9 | style \(LumiTheme { colors }) -> 10 | ( mkLoader 11 | { color: color colors.black1 12 | , radius: px 38 13 | , borderWidth: px 5 14 | } 15 | ) 16 | 17 | spin :: StyleProperty 18 | spin = 19 | keyframes 20 | { from: css { transform: str "rotate(0deg)" } 21 | , to: css { transform: str "rotate(360deg)" } 22 | } 23 | 24 | mkLoader :: 25 | { color :: StyleProperty 26 | , radius :: StyleProperty 27 | , borderWidth :: StyleProperty 28 | } -> 29 | Style 30 | mkLoader { color: c, radius, borderWidth } = 31 | css 32 | { boxSizing: borderBox 33 | , content: str "\"\"" 34 | , display: inlineBlock 35 | , height: radius 36 | , width: radius 37 | , borderWidth: borderWidth 38 | , borderStyle: solid 39 | , borderColor: c 40 | , borderTopColor: str "transparent" 41 | , borderRadius: percent 50.0 42 | , animation: str "1s infinite linear" 43 | , animationName: spin 44 | } 45 | -------------------------------------------------------------------------------- /src/Lumi/Styles/Responsive.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Styles.Responsive where 2 | 3 | import Prelude 4 | 5 | import Lumi.Styles (Style, StyleModifier, css, nested, style, toCSS) 6 | 7 | -- | Create a style modifier that, only in a desktop-sized screen, applies the 8 | -- | styles accumulated in the modifier passed in as argument. 9 | -- | 10 | -- | NOTE: the value passed in as argument must be a props modifier that touches 11 | -- | no component-specific props, a property that currently defines style 12 | -- | modifiers. 13 | onDesktop :: StyleModifier -> StyleModifier 14 | onDesktop m = 15 | style \theme -> 16 | desktopQuery 17 | $ toCSS m 18 | $ theme 19 | 20 | -- | Guard a style so that it's only applied in a desktop resolution. 21 | desktopQuery :: Style -> Style 22 | desktopQuery style = 23 | css 24 | { "@media (min-width: 860px)": nested style 25 | } 26 | 27 | -- | Create a style modifier that, only in a mobile-sized screen, applies the 28 | -- | styles accumulated in the modifier passed in as argument. 29 | -- | 30 | -- | NOTE: the value passed in as argument must be a props modifier that touches 31 | -- | no component-specific props, a property that currently defines style 32 | -- | modifiers. 33 | onMobile :: StyleModifier -> StyleModifier 34 | onMobile m = 35 | style \theme -> 36 | mobileQuery 37 | $ toCSS m 38 | $ theme 39 | 40 | -- | Guard a style so that it's only applied in a mobile screen resolution. 41 | mobileQuery :: Style -> Style 42 | mobileQuery style = 43 | css 44 | { "@media (max-width: 859px)": nested style 45 | } 46 | -------------------------------------------------------------------------------- /src/Lumi/Styles/Slat.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Styles.Slat 2 | ( slat 3 | , slatColumn 4 | , module Border 5 | ) where 6 | 7 | import Prelude 8 | 9 | import Lumi.Components.Spacing (Space(S16)) 10 | import Lumi.Styles (StyleModifier, style_) 11 | import Lumi.Styles.Box (FlexAlign(..), _align, _justify, _row) 12 | import Lumi.Styles.Border (border) 13 | import Lumi.Styles.Border hiding (border) as Border 14 | import React.Basic.Emotion (css, nested, prop, str, unset) 15 | 16 | slat :: StyleModifier 17 | slat = 18 | border 19 | <<< _row 20 | <<< _align Center 21 | <<< _justify SpaceBetween 22 | <<< style_ 23 | ( css 24 | { label: str "slat" 25 | , flex: str "0 0 content" 26 | , color: unset 27 | , backgroundColor: unset 28 | , textDecoration: unset 29 | } 30 | ) 31 | 32 | slatColumn :: Int -> StyleModifier 33 | slatColumn flexGrow = 34 | style_ 35 | $ css 36 | $ { flexGrow: str (show flexGrow) 37 | , "&:not(:first-child)": 38 | nested 39 | $ css 40 | { marginLeft: prop S16 41 | , alignItems: prop End 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Lumi/Styles/Theme.purs: -------------------------------------------------------------------------------- 1 | module Lumi.Styles.Theme where 2 | 3 | import Prelude 4 | 5 | import Data.Int as Int 6 | import Data.Newtype (class Newtype) 7 | import Effect.Unsafe (unsafePerformEffect) 8 | import Lumi.Components.Color (Color, ColorMap, ColorName, colorNames, colors) 9 | import React.Basic.Classic (ReactContext, createContext) 10 | import React.Basic.Hooks (Hook, UseContext, useContext) 11 | 12 | newtype LumiTheme 13 | = LumiTheme 14 | { colors :: ColorMap Color 15 | , colorNames :: ColorMap ColorName 16 | , fontSizes :: TextMap Int 17 | -- | We store the factors for finding the line height and bottom margin for a 18 | -- | given font size instead of actual values so that it's easier to override 19 | -- | the theme with different font sizes while following the Lumi style 20 | -- | guidelines. 21 | , lineHeightFactor :: Number 22 | , textMarginFactor :: Number 23 | } 24 | 25 | type TextMap a = 26 | { subtext :: a 27 | , body :: a 28 | , subSubsectionHeader :: a 29 | , subsectionHeader :: a 30 | , sectionHeader :: a 31 | , title :: a 32 | , mainHeader :: a 33 | } 34 | 35 | derive instance newtypeLumiTheme :: Newtype LumiTheme _ 36 | 37 | defaultTheme :: LumiTheme 38 | defaultTheme = LumiTheme 39 | { colors 40 | , colorNames 41 | , fontSizes: 42 | { subtext: 12 43 | , body: 14 44 | , subSubsectionHeader: 15 45 | , subsectionHeader: 17 46 | , sectionHeader: 20 47 | , title: 24 48 | , mainHeader: 30 49 | } 50 | , lineHeightFactor: 17.0 / 14.0 51 | , textMarginFactor: 9.0 / 17.0 52 | } 53 | 54 | lumiThemeContext :: ReactContext LumiTheme 55 | lumiThemeContext = 56 | unsafePerformEffect do 57 | createContext defaultTheme 58 | 59 | useTheme :: Hook (UseContext LumiTheme) LumiTheme 60 | useTheme = useContext lumiThemeContext 61 | 62 | textFontSize :: LumiTheme -> (forall a. TextMap a -> a) -> Int 63 | textFontSize (LumiTheme { fontSizes }) selector = selector fontSizes 64 | 65 | textLineHeight :: LumiTheme -> (forall a. TextMap a -> a) -> Int 66 | textLineHeight (LumiTheme { fontSizes, lineHeightFactor }) selector = 67 | Int.floor $ Int.toNumber (selector fontSizes) * lineHeightFactor 68 | 69 | textMargin :: LumiTheme -> (forall a. TextMap a -> a) -> Int 70 | textMargin (LumiTheme { fontSizes, textMarginFactor }) selector = 71 | Int.floor $ Int.toNumber (selector fontSizes) * textMarginFactor 72 | --------------------------------------------------------------------------------