├── .editorconfig ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .nvmrc ├── DEVELOPERS.md ├── LICENSE.md ├── README.md ├── airship.svg ├── index.html ├── lerna.json ├── package-lock.json ├── package.json └── packages ├── api ├── package.json ├── src │ ├── SQL.js │ ├── index.js │ └── models │ │ ├── AggregationTypes.js │ │ ├── CategoryModel.js │ │ ├── FilterConditionBuilder.js │ │ └── FormulaModel.js ├── webpack.config.js └── webpack.prod.js └── react-ui ├── .storybook ├── main.js ├── preview-head.html └── preview.js ├── package.json ├── src ├── index.js ├── legends │ ├── CategoryLegendUI.js │ └── ColorBinsLegendUI.js ├── stories │ ├── Introduction.stories.mdx │ ├── common │ │ ├── Autocomplete.stories.js │ │ ├── Breadcrumb.stories.js │ │ ├── Button.stories.js │ │ ├── ButtonGroup.stories.js │ │ ├── Checkbox.stories.js │ │ ├── Divider.stories.js │ │ ├── List.stories.js │ │ ├── Palette.stories.js │ │ ├── Paper.stories.js │ │ ├── Radio.stories.js │ │ ├── Select.stories.js │ │ ├── Switch.stories.js │ │ ├── Tabs.stories.js │ │ ├── Text-field.stories.js │ │ ├── Tooltip.stories.js │ │ └── Typography.stories.js │ └── widgets │ │ ├── CategoryWidgetUI.stories.js │ │ ├── FormulaWidgetUI.stories.js │ │ ├── HistogramWidgetUI.stories.js │ │ └── WrapperWidgetUI.stories.js ├── theme │ └── carto-theme.js ├── utils │ └── rgbToHex.js └── widgets │ ├── CategoryWidgetUI.js │ ├── FormulaWidgetUI.js │ ├── HistogramWidgetUI.js │ └── WrapperWidgetUI.js ├── webpack.config.js └── webpack.prod.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "plugin:react/recommended", 8 | "semistandard" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parserOptions": { 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "ecmaVersion": 2018, 19 | "sourceType": "module" 20 | }, 21 | "plugins": [ 22 | "react" 23 | ], 24 | "rules": { 25 | } 26 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Context 2 | *Please explain here below what you were doing when the issue happened* 3 | 4 | 5 | ### Steps to Reproduce 6 | *Please break down here below all the needed steps to reproduce the issue* 7 | 8 | 1. 9 | 2. 10 | 3. 11 | 12 | ### Current Result 13 | *Please describe here below the current result you got* 14 | 15 | 16 | ### Expected result 17 | *Please describe here below what should be the expected behaviour* 18 | 19 | 20 | ### Browser and version 21 | *What internet browser (Chrome, Firefox, etc) and version was you using and version* 22 | 23 | 24 | ### Additional info 25 | *Please add any information of interest here below* 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **What is happening?** 8 | ... 9 | 10 | **What should happen?** 11 | ... 12 | 13 | **Steps to Reproduce** 14 | ... 15 | 16 | **Device/Browser/Airship-Version** 17 | ... 18 | 19 | **Extra information** 20 | ... 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Problem** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Proposed solution** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Extra info** 14 | ... 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Have questions? We are here to help! 4 | 5 | --- 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fix # 2 | --- -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !packages/icons/dist 2 | .DS_Store 3 | .env 4 | .idea/ 5 | .sass-cache/ 6 | .stencil/ 7 | .versions/ 8 | .vscode/ 9 | *-out.png 10 | *.lock 11 | *.log 12 | *.log 13 | *.sublime-project 14 | *.sublime-workspace 15 | *.sw[mnpcod] 16 | *.tmp 17 | *.tmp.* 18 | *~ 19 | $RECYCLE.BIN/ 20 | dist 21 | log.txt 22 | node_modules 23 | node_modules/ 24 | Thumbs.db 25 | UserInterfaceState.xcuserstate 26 | www/ 27 | secrets.json 28 | 29 | # Packages ignoring items 30 | packages/react-ui/storybook-static -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.16.1 2 | -------------------------------------------------------------------------------- /DEVELOPERS.md: -------------------------------------------------------------------------------- 1 | # DEVELOPER NOTES 2 | 3 | ## Getting started 4 | Clone the repository and execute `yarn`. Then, you are ready to go! 5 | 6 | ``` 7 | git clone git@github.com:CartoDB/airship.git 8 | cd airship 9 | yarn 10 | yarn dev 11 | ``` 12 | 13 | ## Publishing 14 | Build in production mode and publish it at npm: 15 | 16 | ``` 17 | yarn build 18 | npm publish --access public 19 | ``` -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, CARTO 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Airship 2 | 3 | > Airship is a components library built by CARTO 4 | 5 | [![CircleCI](https://circleci.com/gh/CartoDB/airship/tree/master.svg?style=svg)](https://circleci.com/gh/CartoDB/airship/tree/master) 6 | 7 | | module | stable | description | 8 | |---|---|---| 9 | | [airship-api](https://www.npmjs.com/package/@carto/airship-api) | ![npm version](https://badgen.net/npm/v/@carto/airship-style) | A complete CSS framework to build location intelligence apps. | 10 | | [react-airship-ui](https://www.npmjs.com/package/@carto/react-airship-ui) | ![npm version](https://badgen.net/npm/v/@carto/airship-components) | Web components for Location Intelligence apps. | 11 | | [airship-icons](https://www.npmjs.com/package/@carto/airship-icons) | ![npm version](https://badgen.net/npm/v/@carto/airship-icons) | High quality icons set | 12 | 13 | ## What is Airship? 14 | 15 | TBD 16 | 17 | ## License 18 | BSD-3-Clause, see the included [LICENSE.md](LICENSE.md) file. 19 | -------------------------------------------------------------------------------- /airship.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Airship dev center 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Airship

18 | Catalog 19 | Examples 20 | Components 21 | Styles 22 | Bridge 23 | Smoke Tests 24 |
25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.11.0", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "version": "2.4.1" 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@carto/airship", 3 | "version": "0.0.0", 4 | "description": "CARTO Airship framework", 5 | "main": "index.js", 6 | "repository": "git@github.com:CartoDB/airship.git", 7 | "author": "CARTO Dev Team", 8 | "license": "BSD-3-Clause", 9 | "devDependencies": { 10 | "eslint": "^7.11.0", 11 | "eslint-plugin-import": "^2.22.1", 12 | "eslint-plugin-node": "^11.1.0", 13 | "eslint-plugin-promise": "^4.2.1", 14 | "eslint-plugin-react": "^7.21.4", 15 | "eslint-config-semistandard": "^15.0.1", 16 | "eslint-plugin-standard": "^4.0.1", 17 | "lerna": "^3.20.2" 18 | }, 19 | "scripts": { 20 | "build:api": "lerna exec --scope @carto/airship-api npm run build", 21 | "build:react-ui": "lerna exec --scope @carto/react-airship-ui npm run build", 22 | "build": "lerna run --parallel build", 23 | "dev": "lerna run --parallel dev", 24 | "lint:fix": "lerna run lint:fix", 25 | "lint": "lerna run lint", 26 | "postinstall": "lerna bootstrap --hoist", 27 | "publish": "lerna run publish", 28 | "publish:beta": "lerna run publish:beta", 29 | "local-link": "lerna run local-link" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@carto/airship-api", 3 | "version": "3.0.0-alpha4-1", 4 | "description": "Airship UI components for react", 5 | "main": "dist/asapi.js", 6 | "scripts": { 7 | "test": "jest", 8 | "test:watch": "jest --watch", 9 | "lint": "eslint src/**/*.js", 10 | "lint:fix": "eslint src/**/*.js --fix", 11 | "dev": "webpack --watch", 12 | "build": "rm -rf dist && webpack --config webpack.prod.js", 13 | "publish": "npm publish --access public", 14 | "publish:beta": "npm publish --tag beta --access public", 15 | "local-link": "yarn link" 16 | }, 17 | "author": "CARTO Dev Team", 18 | "license": "BSD-3-Clause", 19 | "devDependencies": { 20 | "jest": "23.6.0", 21 | "webpack": "^4.27.0", 22 | "webpack-cli": "^3.1.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/api/src/SQL.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_USER_COMPONENT_IN_URL = '{user}'; 2 | const REQUEST_GET_MAX_URL_LENGTH = 2048; 3 | 4 | export const execute = async (query, credentials) => { 5 | let response; 6 | 7 | try { 8 | const request = createRequest(query, credentials); 9 | /* global fetch */ 10 | /* eslint no-undef: "error" */ 11 | response = await fetch(request); 12 | } catch (error) { 13 | throw new Error(`Failed to connect to SQL API: ${error}`); 14 | } 15 | 16 | const data = await response.json(); 17 | 18 | if (!response.ok) { 19 | dealWithError({ response, data, credentials }); 20 | } 21 | 22 | return data.rows; 23 | }; 24 | 25 | /** 26 | * Display proper message from SQL API error 27 | */ 28 | function dealWithError({ response, data, credentials }) { 29 | switch (response.status) { 30 | case 401: 31 | throw new Error( 32 | `Unauthorized access to SQL API: invalid combination of user ('${credentials.username}') and apiKey ('${credentials.apiKey}')` 33 | ); 34 | case 403: 35 | throw new Error( 36 | `Unauthorized access to dataset: the provided apiKey('${credentials.apiKey}') doesn't provide access to the requested data` 37 | ); 38 | default: 39 | throw new Error(`${JSON.stringify(data.error)}`); 40 | } 41 | } 42 | 43 | /** 44 | * Create a GET or POST request, with all required parameters 45 | */ 46 | function createRequest(query, credentials) { 47 | const encodedApiKey = encodeParameter('api_key', credentials.apiKey); 48 | const encodedClient = encodeParameter('client', credentials.username); 49 | const parameters = [encodedApiKey, encodedClient]; 50 | const queryParameter = encodeParameter('q', query); 51 | const url = generateSqlApiUrl(parameters, credentials); 52 | const getUrl = `${url}&${queryParameter}`; 53 | 54 | if (getUrl.length < REQUEST_GET_MAX_URL_LENGTH) { 55 | return getRequest(getUrl); 56 | } 57 | 58 | return postRequest(url, queryParameter); 59 | } 60 | 61 | /** 62 | * Generate a SQL API url for the request 63 | */ 64 | function generateSqlApiUrl(parameters, credentials) { 65 | const base = `${serverURL(credentials)}api/v2/sql`; 66 | return `${base}?${parameters.join('&')}`; 67 | } 68 | 69 | /** 70 | * Prepare a url valid for the specified user 71 | */ 72 | function serverURL(credentials) { 73 | let url = credentials.serverUrlTemplate.replace( 74 | DEFAULT_USER_COMPONENT_IN_URL, 75 | credentials.username 76 | ); 77 | 78 | if (!url.endsWith('/')) { 79 | url += '/'; 80 | } 81 | 82 | return url; 83 | } 84 | 85 | /** 86 | * Simple GET request 87 | */ 88 | function getRequest(url) { 89 | /* global Request */ 90 | /* eslint no-undef: "error" */ 91 | return new Request(url, { 92 | method: 'GET', 93 | headers: { 94 | Accept: 'application/json', 95 | }, 96 | }); 97 | } 98 | 99 | /** 100 | * Simple POST request 101 | */ 102 | function postRequest(url, payload) { 103 | return new Request(url, { 104 | method: 'POST', 105 | headers: { 106 | Accept: 'application/json', 107 | 'Content-Type': 'application/json', 108 | }, 109 | body: payload, 110 | }); 111 | } 112 | 113 | /** 114 | * Simple encode parameter 115 | */ 116 | function encodeParameter(name, value) { 117 | return `${name}=${encodeURIComponent(value)}`; 118 | } -------------------------------------------------------------------------------- /packages/api/src/index.js: -------------------------------------------------------------------------------- 1 | import { execute } from './SQL'; 2 | import { AggregationTypes } from './models/AggregationTypes'; 3 | import { getCategories } from './models/CategoryModel'; 4 | import { getValue } from './models/FormulaModel'; 5 | import { FilterTypes, getFilterCondition, getConditionFromViewPort, getFilteredQuery } from './models/FilterConditionBuilder'; 6 | 7 | export { 8 | execute, 9 | AggregationTypes, 10 | FilterTypes, 11 | getCategories, 12 | getValue, 13 | getFilterCondition, 14 | getConditionFromViewPort, 15 | getFilteredQuery 16 | }; -------------------------------------------------------------------------------- /packages/api/src/models/AggregationTypes.js: -------------------------------------------------------------------------------- 1 | export const AggregationTypes = Object.freeze({ 2 | COUNT: 'count', 3 | AVG: 'avg', 4 | MIN: 'min', 5 | MAX: 'max', 6 | SUM: 'sum', 7 | }); -------------------------------------------------------------------------------- /packages/api/src/models/CategoryModel.js: -------------------------------------------------------------------------------- 1 | import { execute } from '../SQL'; 2 | import { getFilterCondition, getConditionFromViewPort } from './FilterConditionBuilder'; 3 | 4 | export const getCategories = (props) => { 5 | const { 6 | data, 7 | credentials, 8 | column, 9 | operation, 10 | 'operation-column': operationColumn, 11 | filters, 12 | viewport, 13 | } = props; 14 | 15 | if (Array.isArray(data)) { 16 | throw new Error('Array is not a valid type to get categories'); 17 | } 18 | 19 | let query = 20 | (viewport && 21 | `SELECT * FROM (${data}) as q WHERE ${getConditionFromViewPort(viewport)}`) || 22 | data; 23 | 24 | query = `WITH all_categories as ( 25 | SELECT ${column} as category 26 | FROM (${query}) as q 27 | GROUP BY category 28 | ), 29 | categories as ( 30 | SELECT ${column} as category, ${operation}(${operationColumn}) as value 31 | FROM (${query}) as q 32 | ${getFilterCondition(filters)} 33 | GROUP BY category 34 | ) 35 | SELECT a.category, b.value 36 | FROM all_categories a 37 | LEFT JOIN categories b ON a.category=b.category 38 | ORDER BY value DESC NULLS LAST;`; 39 | 40 | return execute(query, credentials); 41 | }; -------------------------------------------------------------------------------- /packages/api/src/models/FilterConditionBuilder.js: -------------------------------------------------------------------------------- 1 | export const FilterTypes = Object.freeze({ 2 | IN: 'in', 3 | }); 4 | 5 | export const getFilterCondition = (filters = {}) => { 6 | const result = []; 7 | 8 | Object.entries(filters).forEach(([column, filter]) => { 9 | Object.entries(filter).forEach(([operator, values]) => { 10 | switch (operator) { 11 | case 'in': 12 | result.push(`${column} ${operator}(${values.map((v) => `'${v}'`).join(',')})`); 13 | break; 14 | default: 15 | throw new Error(`Not valid operator has provided: ${operator}`); 16 | } 17 | }); 18 | }); 19 | 20 | return result.length ? `WHERE ${result.join(' AND ')}` : ''; 21 | }; 22 | 23 | export const getConditionFromViewPort = (viewport) => { 24 | return `ST_Intersects( 25 | the_geom_webmercator, 26 | ST_Transform(ST_MakeEnvelope(${viewport.join(',')}, 4326), 3857) 27 | )`; 28 | }; 29 | 30 | export const getFilteredQuery = ({ data, filters }) => { 31 | return ` 32 | SELECT * 33 | FROM (${data}) as q 34 | ${getFilterCondition(filters)} 35 | `; 36 | }; -------------------------------------------------------------------------------- /packages/api/src/models/FormulaModel.js: -------------------------------------------------------------------------------- 1 | import { execute } from '../SQL'; 2 | import { getFilterCondition, getConditionFromViewPort } from './FilterConditionBuilder'; 3 | 4 | export const getValue = (props) => { 5 | const { 6 | data, 7 | credentials, 8 | operation, 9 | 'operation-column': operationColumn, 10 | filters, 11 | viewport, 12 | } = props; 13 | 14 | if (Array.isArray(data)) { 15 | throw new Error('Array is not a valid type to get categories'); 16 | } 17 | 18 | let query = 19 | (viewport && 20 | `SELECT * FROM (${data}) as q WHERE ${getConditionFromViewPort(viewport)}`) || 21 | data; 22 | 23 | query = ` 24 | SELECT ${operation}(${operationColumn}) as value 25 | FROM (${query}) as q 26 | ${getFilterCondition(filters)} 27 | `; 28 | 29 | return execute(query, credentials); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/api/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: './src/index.js', 6 | resolve: { 7 | extensions: [ '.js' ] 8 | }, 9 | output: { 10 | path: path.resolve(__dirname, 'dist'), 11 | filename: 'asapi.js', 12 | library: 'AsApi', 13 | libraryTarget: 'umd' 14 | } 15 | }; -------------------------------------------------------------------------------- /packages/api/webpack.prod.js: -------------------------------------------------------------------------------- 1 | // const path = require('path'); 2 | const config = require('./webpack.config.js'); 3 | 4 | config.mode = 'production'; 5 | 6 | module.exports = config; 7 | -------------------------------------------------------------------------------- /packages/react-ui/.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../src/**/*.stories.mdx", 4 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials" 9 | ] 10 | } -------------------------------------------------------------------------------- /packages/react-ui/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/react-ui/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ThemeProvider } from '@material-ui/core'; 3 | import { createTheme } from '../src/theme/carto-theme' 4 | 5 | const theme = createTheme(); 6 | 7 | export const decorators = [ 8 | (Story) => ( 9 | 10 | 11 | 12 | ), 13 | ]; 14 | 15 | export const parameters = { 16 | actions: { argTypesRegex: "^on[A-Z].*" }, 17 | viewMode: 'docs', 18 | options: { 19 | storySort: { 20 | order: [ 21 | 'Introduction', 22 | 'Getting Started', 23 | [ 24 | 'Palette', 25 | 'Typography' 26 | ], 27 | 'Common', 28 | 'Widgets', 29 | [ 30 | 'WrapperWidgetUI', 31 | 'FormulaWidgetUI', 32 | 'CategoryWidgetUI' 33 | ] 34 | ] 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /packages/react-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@carto/react-airship-ui", 3 | "version": "3.0.0-alpha5", 4 | "description": "Airship CARTO API utilities", 5 | "main": "dist/asreactui.js", 6 | "scripts": { 7 | "test": "jest", 8 | "test:watch": "jest --watch", 9 | "lint": "eslint 'src/**/*.{js,jsx}'", 10 | "lint:fix": "eslint 'src/**/*.{js,jsx}' --fix", 11 | "dev": "webpack --watch", 12 | "build": "rm -rf dist && webpack --config webpack.prod.js", 13 | "publish": "npm publish --access public", 14 | "publish:beta": "npm publish --tag beta --access public", 15 | "storybook": "start-storybook -p 6006", 16 | "build-storybook": "build-storybook", 17 | "publish-storybook": "build-storybook && cd storybook-static && npx vercel --prod --name 'react-airship-ui'", 18 | "publish-storybook-staging": "build-storybook && cd storybook-static && npx vercel --prod --name 'react-airship-ui-staging'", 19 | "local-link": "yarn link" 20 | }, 21 | "author": "CARTO Dev Team", 22 | "license": "BSD-3-Clause", 23 | "devDependencies": { 24 | "@babel/core": "^7.12.1", 25 | "@babel/preset-env": "^7.12.1", 26 | "@babel/preset-react": "^7.12.1", 27 | "@material-ui/core": "^4.11.0", 28 | "@material-ui/lab": "^4.0.0-alpha.56", 29 | "@material-ui/icons": "^4.9.1", 30 | "@storybook/addon-actions": "^6.0.27", 31 | "@storybook/addon-essentials": "^6.0.27", 32 | "@storybook/addon-links": "^6.0.27", 33 | "@storybook/react": "^6.0.27", 34 | "babel-loader": "^8.1.0", 35 | "echarts": "^4.9.0", 36 | "echarts-for-react": "^2.0.16", 37 | "jest": "23.6.0", 38 | "prop-types": "^15.7.2", 39 | "react": "^16.14.0", 40 | "react-is": "^17.0.1", 41 | "webpack": "^4.27.0", 42 | "webpack-cli": "^3.1.2" 43 | }, 44 | "dependencies": {} 45 | } 46 | -------------------------------------------------------------------------------- /packages/react-ui/src/index.js: -------------------------------------------------------------------------------- 1 | import { cartoOptions, createTheme } from './theme/carto-theme'; 2 | import WrapperWidgetUI from './widgets/WrapperWidgetUI'; 3 | import CategoryWidgetUI from './widgets/CategoryWidgetUI'; 4 | import FormulaWidgetUI from './widgets/FormulaWidgetUI'; 5 | import HistogramWidgetUI from './widgets/HistogramWidgetUI'; 6 | import CategoryLegendUI from './legends/CategoryLegendUI'; 7 | import ColorBinsLegendUI from './legends/ColorBinsLegendUI'; 8 | 9 | export { 10 | cartoOptions, 11 | createTheme, 12 | WrapperWidgetUI, 13 | CategoryWidgetUI, 14 | FormulaWidgetUI, 15 | HistogramWidgetUI, 16 | CategoryLegendUI, 17 | ColorBinsLegendUI, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/react-ui/src/legends/CategoryLegendUI.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid, Paper, Typography, makeStyles } from '@material-ui/core'; 3 | import rgbToHex from '../utils/rgbToHex'; 4 | 5 | const useStyles = makeStyles((theme) => ({ 6 | root: { 7 | ...theme.typography.caption, 8 | padding: theme.spacing(1.5), 9 | backgroundColor: theme.palette.common.white, 10 | }, 11 | 12 | element: { 13 | ...theme.typography.overline, 14 | textTransform: 'none', 15 | color: theme.palette.text.secondary, 16 | padding: theme.spacing(0.25, 0), 17 | }, 18 | 19 | dot: { 20 | flex: '0 0 auto', 21 | width: 8, 22 | height: 8, 23 | marginRight: theme.spacing(1), 24 | }, 25 | })); 26 | 27 | function CategoryLegendUI(props) { 28 | const classes = useStyles(); 29 | const { categories } = props; 30 | 31 | return ( 32 | 33 | {categories.title} 34 | {Object.entries(categories.colors).map((elem, i) => ( 35 | 42 |
49 | {categories.labels[elem[0]]} 50 |
51 | ))} 52 |
53 | ); 54 | } 55 | 56 | export default CategoryLegendUI; 57 | -------------------------------------------------------------------------------- /packages/react-ui/src/legends/ColorBinsLegendUI.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function ColorBinsLegendUI(props) { 4 | const { colorScale, minValue, maxValue } = props; 5 | 6 | return ( 7 |
8 |

{minValue}

9 | {colorScale.forEach((elem) => ( 10 |
11 | ))} 12 |

{maxValue}

13 |
14 | ); 15 | } 16 | 17 | export default ColorBinsLegendUI; 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/stories/Introduction.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs/blocks'; 2 | 3 | 4 | 5 | # Airship UI for React (v3.0.0-alpha5) 6 | 7 | ## How to add a new Story 8 | Put your `stories.js` files inside the `src/stories` folder following the same folder structure as the source files. For example: 9 | `stories/widgets/FormulaWidgetUI.stories.js` for `src/widgets/FormulaWidgetUI.js` component. 10 | 11 | As long as we are using a decorator to inject the MaterialUI theme, you just need to import and add your component to the `template` of your story. 12 | ```js 13 | import React from 'react'; 14 | import FormulaWidgetUI from '../../widgets/FormulaWidgetUI'; 15 | 16 | const Template = (args) => ; 17 | ``` 18 | 19 | Then, export your story and the different variations: 20 | ```js 21 | export default { 22 | title: 'Widgets/FormulaWidgetUI', 23 | component: FormulaWidgetUI, 24 | }; 25 | 26 | export const Empty = Template.bind({}); 27 | Empty.args = {}; 28 | 29 | export const Text = Template.bind({}); 30 | Text.args = { data: '$1000000'}; 31 | ``` 32 | 33 | Optionally, you can add some extra controls to your story, but by default Storybook will add automatic controls for your story arguments. -------------------------------------------------------------------------------- /packages/react-ui/src/stories/common/Autocomplete.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Autocomplete from '@material-ui/lab/Autocomplete'; 3 | import { Grid, TextField } from '@material-ui/core'; 4 | 5 | export default { 6 | title: 'Common/Autocomplete', 7 | component: Autocomplete, 8 | argTypes: { 9 | variant: { 10 | control: { 11 | type: 'select', 12 | options: ['standard', 'filled', 'outlined'] 13 | } 14 | }, 15 | color: { 16 | control: { 17 | type: 'select', 18 | options: ['default', 'primary', 'secondary'] 19 | } 20 | }, 21 | required: { 22 | control: { 23 | type: 'boolean' 24 | } 25 | }, 26 | disabled: { 27 | control: { 28 | type: 'boolean' 29 | } 30 | } 31 | } 32 | } 33 | 34 | const top100Films = [ 35 | { title: 'The Shawshank Redemption', year: 1994 }, 36 | { title: 'The Godfather', year: 1972 }, 37 | { title: 'The Godfather: Part II', year: 1974 }, 38 | { title: 'The Dark Knight', year: 2008 }, 39 | { title: '12 Angry Men', year: 1957 }, 40 | { title: "Schindler's List", year: 1993 }, 41 | { title: 'Pulp Fiction', year: 1994 }, 42 | { title: 'The Lord of the Rings: The Return of the King', year: 2003 }, 43 | { title: 'The Good, the Bad and the Ugly', year: 1966 }, 44 | { title: 'Fight Club', year: 1999 }, 45 | { title: 'The Lord of the Rings: The Fellowship of the Ring', year: 2001 }, 46 | { title: 'Star Wars: Episode V - The Empire Strikes Back', year: 1980 }, 47 | { title: 'Forrest Gump', year: 1994 }, 48 | { title: 'Inception', year: 2010 }, 49 | { title: 'The Lord of the Rings: The Two Towers', year: 2002 } 50 | ]; 51 | 52 | const Template = ({ label = 'Choose films', disabled, ...args}) => ( 53 | option.title} 57 | disabled={disabled} 58 | renderInput={(params) => ()} 59 | /> 60 | ); 61 | 62 | const AutocompleteTemplate = ({ disabled, size, ...args }) => { 63 | const options = top100Films.map((option) => { 64 | const firstLetter = option.title[0].toUpperCase(); 65 | return { 66 | firstLetter: /[0-9]/.test(firstLetter) ? '0-9' : firstLetter, 67 | ...option, 68 | }; 69 | }); 70 | 71 | return ( 72 | 73 | 74 | 75 | option.title} 79 | disabled={disabled} 80 | renderInput={(params) => ()} 81 | size={size} 82 | /> 83 | 84 | 85 | -b.firstLetter.localeCompare(a.firstLetter))} 88 | groupBy={(option) => option.firstLetter} 89 | getOptionLabel={(option) => option.title} 90 | disabled={disabled} 91 | renderInput={(params) => ()} 92 | size={size} 93 | /> 94 | 95 | 96 | 97 | 98 | option.title} 103 | disabled={disabled} 104 | renderInput={(params) => ()} 105 | size={size} 106 | /> 107 | 108 | 109 | 110 | ); 111 | }; 112 | 113 | export const Playground = Template.bind({}); 114 | 115 | export const Default = AutocompleteTemplate.bind({}); 116 | Default.args = { } 117 | 118 | export const Small = AutocompleteTemplate.bind({}); 119 | Small.args = { size: 'small' } 120 | -------------------------------------------------------------------------------- /packages/react-ui/src/stories/common/Breadcrumb.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Breadcrumbs, Link, Typography } from '@material-ui/core'; 3 | import { NavigateNext, CloudCircle, Home, Style } from '@material-ui/icons'; 4 | 5 | export default { 6 | title: 'Common/Breadcrumbs', 7 | component: Breadcrumbs, 8 | argTypes: { 9 | maxItems: { 10 | control: { 11 | type: 'number' 12 | } 13 | } 14 | } 15 | } 16 | 17 | const Template = ({ ...args }) => ( 18 | } {...args}> 19 | 20 | CARTO 21 | 22 | 23 | Airship 24 | 25 | Storybook 26 | 27 | ); 28 | 29 | const WithIconsTemplate = ({ ...args }) => ( 30 | } {...args}> 31 | 32 | 33 | CARTO 34 | 35 | 36 | 37 | Airship 38 | 39 | 40 |