diff --git a/.eslintrc b/.eslintrc index ca304846a..dba6b3d3a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,3 @@ -{ - "extends": ["@deity/eslint-config-falcon"] -} +{ + "extends": ["@deity/eslint-config-falcon"] +} diff --git a/.lintstagedrc b/.lintstagedrc index b6baf6a44..ae9ff6188 100644 --- a/.lintstagedrc +++ b/.lintstagedrc @@ -1,4 +1,4 @@ "linters": { - "*.{js,jxs}": ["eslint --fix", "git add"], + "*.{js,jxs,ts, tsx}": ["eslint --fix", "git add"], "*.{json,css,scss,graphql,yml}": ["prettier --write", "git add"] } \ No newline at end of file diff --git a/packages/falcon-dev-tools/eslint-config-falcon/index.js b/packages/falcon-dev-tools/eslint-config-falcon/index.js index 9a584870f..a387caea9 100644 --- a/packages/falcon-dev-tools/eslint-config-falcon/index.js +++ b/packages/falcon-dev-tools/eslint-config-falcon/index.js @@ -3,7 +3,11 @@ module.exports = { extends: ['eslint-config-airbnb', 'plugin:prettier/recommended'], plugins: ['react', 'import', 'prettier'], settings: { - 'import/parser': 'babel-eslint' + 'import/parser': 'babel-eslint', + 'import/resolver': { + node: true, + 'eslint-import-resolver-typescript': true + } }, env: { browser: true, @@ -27,12 +31,7 @@ module.exports = { 'import/no-unresolved': 'off', 'import/no-named-as-default': 'error', 'import/extensions': ['off', 'never'], - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: ['**/__tests__/*', '**/*.test.js', '**/webpack/*.js'] - } - ], + 'import/no-extraneous-dependencies': ['error'], 'jsx-a11y/anchor-is-valid': [ 'off', { @@ -77,7 +76,7 @@ module.exports = { 'react/jsx-filename-extension': [ 1, { - extensions: ['.js', '.jsx'] + extensions: ['.js', '.jsx', '.tsx'] } ], 'react/no-danger': 'off', @@ -99,5 +98,25 @@ module.exports = { __DEVTOOLS__: true, socket: true, webpackIsomorphicTools: true - } + }, + overrides: [ + { + files: ['**/*.ts', '**/*.tsx'], + parser: 'typescript-eslint-parser', + plugins: ['typescript'], + rules: { + 'no-undef': 'off', + 'no-unused-vars': 'off', + 'no-restricted-globals': 'off', + 'react/prefer-stateless-function': 'off', + 'react/react-in-jsx-scope': 'off', + 'no-use-before-define': 'off', + 'no-continue': 'off', + 'dot-notation': 'off', + 'react/prop-types': 'off', + 'import/prefer-default-export': 'off', + 'import-name': [true, { react: 'React' }] + } + } + ] }; diff --git a/packages/falcon-dev-tools/eslint-config-falcon/package.json b/packages/falcon-dev-tools/eslint-config-falcon/package.json index 262bbfe0f..38378cba9 100644 --- a/packages/falcon-dev-tools/eslint-config-falcon/package.json +++ b/packages/falcon-dev-tools/eslint-config-falcon/package.json @@ -9,9 +9,13 @@ "eslint-config-airbnb": "^16.0.0", "eslint-config-prettier": "^2.9.0", "eslint-plugin-import": "^2.13.0", + "eslint-import-resolver-typescript": "^1.0.2", "eslint-plugin-jsx-a11y": "^6.1.1", "eslint-plugin-prettier": "^2.6.2", "eslint-plugin-react": "^7.10.0", + "eslint-plugin-typescript": "^0.12.0", + "typescript": "^3.0.1", + "typescript-eslint-parser": "^18.0.0", "prettier": "^1.14.0" }, "peerDependencies": { diff --git a/packages/falcon-ui/.gitignore b/packages/falcon-ui/.gitignore new file mode 100644 index 000000000..9aad17255 --- /dev/null +++ b/packages/falcon-ui/.gitignore @@ -0,0 +1,3 @@ +.docz/ +dist/ +node_modules/ \ No newline at end of file diff --git a/packages/falcon-ui/.size-snapshot.json b/packages/falcon-ui/.size-snapshot.json new file mode 100644 index 000000000..1956ee5d7 --- /dev/null +++ b/packages/falcon-ui/.size-snapshot.json @@ -0,0 +1,35 @@ +{ + "dist/falcon-ui.cjs.js": { + "bundled": 15553, + "minified": 7559, + "gzipped": 2747 + }, + "dist/falcon-ui.es.js": { + "bundled": 2149, + "minified": 937, + "gzipped": 506, + "treeshaked": { + "rollup": { + "code": 689, + "import_statements": 57 + }, + "webpack": { + "code": 1753 + } + } + }, + "dist/falcon-ui.esm.js": { + "bundled": 15117, + "minified": 7200, + "gzipped": 2662, + "treeshaked": { + "rollup": { + "code": 6240, + "import_statements": 146 + }, + "webpack": { + "code": 7410 + } + } + } +} diff --git a/packages/falcon-ui/README.md b/packages/falcon-ui/README.md new file mode 100644 index 000000000..fcb59f04c --- /dev/null +++ b/packages/falcon-ui/README.md @@ -0,0 +1,53 @@ +// rename docs na docs helpers? wyniesc ponad src? chyba tak +// komponent hide? + +2 docz - dodac custom tsconfig, jak? +// jak powinna wygladac tabelka z props? design + +// Dodać conventional commits? +// TODO: +// 1 obsluga refs przetestowac + +// dodac docsy dla flex, css, theme, root itp +// tak powinien wyglądać doc z theme - https://pricelinelabs.github.io/design-system/Color plus edycja? tak jak tu? https://material-ui.com/style/color/#color-tool albo tu https://material.io/tools/color/#!/?view.left=0&view.right=0 + +//2 mozna dodac komentarze do props mapping i to zadziała! +//4 docz hot reload? nie dziala? +// UKRYC PROPERTY THEME Z PROPS? TAK JSDOC HIDDEN? + +// czwartek +// dodac component card? - ma box shadow? + +// TODO: dodac ratio do theme props? sprobowac ? + +https://github.com/prettier/prettier/pull/4975 + +// Track https://github.com/frenic/csstype/issues/8#issuecomment-403489436 + +/////////// +/////////// +/////////// +OTHER TODO: +// w projekcie klienckim będzie getTheme, ktory zwraca i nadpisuje default theme, plus strona z edycją themu +// kazda kliencka aplikacja ma stronę /\_theme ktora pokazuje jaka jest obecna definicja dla theme'a oraz dodaje mozliwosc zmiany +Components - ogolne/generyczne +Containers - product card, product list etc, checkout +// dodać https://github.com/Andarist/babel-plugin-annotate-pure-calls ? +// https://www.npmjs.com/package/tinycolor2 +// https://reactjs.org/blog/2018/03/29/react-v-16-3.html#strictmode-component +// https://github.com/final-form/react-final-form +// scenariusze modyfikacji +// edycja istniejących zmiennych +// dodanie nowej zmiennej (kolor) +// dodanie nowego komponentu +https://github.com/cimdalli/mui-theme-generator +// TODO: odpalanie build przy install? jak? +// TODO: dzialajace testy jest +// TODO: dokumentacja +// opisac jak mozna modyfikować theme +// Themowalne propsy trzymam w theme, łatwo można dodawać nowe themes, osobna paczka z nowym themem, +// - css custom prop (moze przyjmowac funkcje) +// - as props +// - className fallback +// - przyjmowanie wszystkich propsow +// falcon abstractor i dominator diff --git a/packages/falcon-ui/babel.config.js b/packages/falcon-ui/babel.config.js new file mode 100644 index 000000000..c3f3dea74 --- /dev/null +++ b/packages/falcon-ui/babel.config.js @@ -0,0 +1,25 @@ +const { BABEL_ENV, NODE_ENV } = process.env; +const cjs = BABEL_ENV === 'cjs' || NODE_ENV === 'test'; + +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + modules: false, + loose: true, + targets: { + // > 0.5%, last 2 versions, Firefox ESR, not dead + browsers: 'defaults' + } + } + ], + '@babel/preset-typescript', + '@babel/preset-react' + ], + plugins: [ + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-object-rest-spread', + cjs && 'transform-es2015-modules-commonjs' + ].filter(Boolean) +}; diff --git a/packages/falcon-ui/doczrc.js b/packages/falcon-ui/doczrc.js new file mode 100644 index 000000000..26ce2a093 --- /dev/null +++ b/packages/falcon-ui/doczrc.js @@ -0,0 +1,10 @@ +module.exports = { + typescript: true, + src: './src', + wrapper: 'src/docs/Wrapper', + modifyBundlerConfig: config => { + console.log(config); + + return config; + } +}; diff --git a/packages/falcon-ui/index.js b/packages/falcon-ui/index.js deleted file mode 100644 index d9ffaea29..000000000 --- a/packages/falcon-ui/index.js +++ /dev/null @@ -1 +0,0 @@ -console.log('@deity/falcon-ui hello!'); diff --git a/packages/falcon-ui/jest.config.js b/packages/falcon-ui/jest.config.js new file mode 100644 index 000000000..91780cd72 --- /dev/null +++ b/packages/falcon-ui/jest.config.js @@ -0,0 +1,11 @@ +const { defaults } = require('jest-config'); + +module.exports = { + snapshotSerializers: ['jest-serializer-html'], + coveragePathIgnorePatterns: ['/node_modules/', '<rootDir>/docs'], + moduleFileExtensions: [...defaults.moduleFileExtensions, 'ts', 'tsx'], + transform: { + // (.js, .ts, .jsx, .tsx) files + '^.+\\.(j|t)sx?$': 'babel-jest' + } +}; diff --git a/packages/falcon-ui/package.json b/packages/falcon-ui/package.json index 6862743d1..f2dfe3e04 100644 --- a/packages/falcon-ui/package.json +++ b/packages/falcon-ui/package.json @@ -1,7 +1,62 @@ { "name": "@deity/falcon-ui", "license": "OSL-3.0", - "main": "index.js", "version": "1.0.0", - "repository": "git@github.com:deity-io/falcon.git" + "repository": "git@github.com:deity-io/falcon.git", + "main": "dist/falcon-ui.cjs.js", + "module": "dist/falcon-ui.esm.js", + "types": "dist/index.d.ts", + "source": "src/index.ts", + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "peerDependencies": { + "react": "^16.4.2", + "react-dom": "^16.4.2" + }, + "scripts": { + "prebuild": "rimraf dist", + "build": "rollup -c & tsc & docz build", + "start": "docz dev", + "test": "jest" + }, + "devDependencies": { + "@babel/cli": "^7.0.0-rc.1", + "@babel/core": "^7.0.0-rc.1", + "@babel/plugin-proposal-class-properties": "^7.0.0-rc.1", + "@babel/plugin-proposal-object-rest-spread": "^7.0.0-rc.1", + "@babel/preset-env": "^7.0.0-rc.1", + "@babel/preset-react": "^7.0.0-rc.1", + "@babel/preset-typescript": "^7.0.0-rc.1", + "@types/deepmerge": "^2.1.0", + "@types/jest": "^23.3.1", + "@types/react": "^16.4.9", + "babel-jest": "^23.4.2", + "babel-loader": "8.0.0-beta.4", + "babel-plugin-module-resolver": "^3.1.1", + "docz": "^0.10.3", + "jest-cli": "^23.4.2", + "jest-config": "^23.4.2", + "jest-serializer-html": "^5.0.0", + "react": "^16.4.2", + "react-dom": "^16.4.2", + "react-test-renderer": "^16.4.2", + "rimraf": "^2.6.1", + "rollup": "^0.64.1", + "rollup-plugin-babel": "^4.0.0-beta.8", + "rollup-plugin-node-resolve": "^3.3.0", + "rollup-plugin-size-snapshot": "^0.6.1", + "typescript": "^3.0.1" + }, + "dependencies": { + "@emotion/core": "^0.13.0", + "@emotion/is-prop-valid": "^0.6.5", + "@emotion/provider": "^0.11.1", + "@emotion/styled-base": "^0.10.3", + "@mdx-js/tag": "^0.15.0-2", + "csstype": "^2.5.6", + "deepmerge": "^2.1.1" + } } diff --git a/packages/falcon-ui/rollup.config.js b/packages/falcon-ui/rollup.config.js new file mode 100644 index 000000000..3d38ba9bc --- /dev/null +++ b/packages/falcon-ui/rollup.config.js @@ -0,0 +1,27 @@ +import babel from 'rollup-plugin-babel'; +import resolve from 'rollup-plugin-node-resolve'; +import { sizeSnapshot } from 'rollup-plugin-size-snapshot'; +import pkg from './package.json'; + +const externals = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})]; + +const makeExternalPredicate = externalsArr => { + if (externalsArr.length === 0) { + return () => false; + } + const externalPattern = new RegExp(`^(${externalsArr.join('|')})($|/)`); + return id => externalPattern.test(id); +}; + +export default { + input: 'src/index.ts', + external: makeExternalPredicate(externals), + plugins: [ + resolve({ + extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'] + }), + babel(), + sizeSnapshot({ matchSnapshot: false }) + ], + output: [{ file: pkg.main, format: 'cjs', sourcemap: true }, { file: pkg.module, format: 'esm', sourcemap: true }] +}; diff --git a/packages/falcon-ui/src/components/Button.mdx b/packages/falcon-ui/src/components/Button.mdx new file mode 100644 index 000000000..4677c1edb --- /dev/null +++ b/packages/falcon-ui/src/components/Button.mdx @@ -0,0 +1,35 @@ +--- +name: Button +menu: Components +--- + +import { Playground } from 'docz' +import { PropsTable } from '../docs/PropsTable' +import { Button, GridLayout, Card } from '../' + +# Button +description + +### Best practices +TODO + +### Basic usage +<Playground> + <GridLayout gridTemplateColumns={{ xs: '1fr 2fr', md: '2fr 2fr' }}> + <Card> + <Button bg={{ xs: 'primary', md: 'secondary' }}>Hello!</Button> + </Card> + <Card> + <Button>World</Button> + </Card> + <Card> + <Button>test</Button> + </Card> + <Card> + <Button variant="primary">test 2</Button> + </Card> +</GridLayout> +</Playground> + +### Properties +<PropsTable of={Button} /> diff --git a/packages/falcon-ui/src/components/Button.tsx b/packages/falcon-ui/src/components/Button.tsx new file mode 100644 index 000000000..51cd354f3 --- /dev/null +++ b/packages/falcon-ui/src/components/Button.tsx @@ -0,0 +1,16 @@ +import { themed } from '../theme'; + +export const Button = themed({ + themeKey: 'button', + as: 'button' +})({ + fontFamily: 'inherit', + WebkitFontSmoothing: 'antialiased', + display: 'inline-block', + border: 'none', + textDecoration: 'none', + appearance: 'none', + ':focus': { + outline: 'none' + } +}); diff --git a/packages/falcon-ui/src/components/Card.tsx b/packages/falcon-ui/src/components/Card.tsx new file mode 100644 index 000000000..2253e42fa --- /dev/null +++ b/packages/falcon-ui/src/components/Card.tsx @@ -0,0 +1,6 @@ +import { themed } from '../theme'; + +export const Card = themed({ + themeKey: 'card', + as: 'div' +})(); diff --git a/packages/falcon-ui/src/components/FlexLayout.tsx b/packages/falcon-ui/src/components/FlexLayout.tsx new file mode 100644 index 000000000..e7b907fe7 --- /dev/null +++ b/packages/falcon-ui/src/components/FlexLayout.tsx @@ -0,0 +1,6 @@ +import { themed } from '../theme'; + +export const FlexLayout = themed({ + themeKey: 'flexLayout', + as: 'div' +})(); diff --git a/packages/falcon-ui/src/components/GridLayout.mdx b/packages/falcon-ui/src/components/GridLayout.mdx new file mode 100644 index 000000000..e335aba2b --- /dev/null +++ b/packages/falcon-ui/src/components/GridLayout.mdx @@ -0,0 +1,41 @@ +--- +name: GridLayout +menu: Components +--- + +import { Playground } from 'docz' +import { PropsTable } from '../docs/PropsTable' +import { GridLayout, Card } from '../' + +# Grid Layout +description + +### Best practices +TODO + +### Basic usage +<Playground> +<GridLayout gridTemplateColumns={{ xs: '1fr 2fr', md: '2fr 2fr' }} gridTemplateRows='1fr 50vh 1fr'> + <Card> + c 1 + </Card> + <Card> + c 2 + </Card> + <Card> + c 3 + </Card> + <Card> + c 4 + </Card> + <Card> + c 5 + </Card> + <Card> + c 6 + </Card> +</GridLayout> +</Playground> + +### Properties +<PropsTable of={GridLayout} /> diff --git a/packages/falcon-ui/src/components/GridLayout.tsx b/packages/falcon-ui/src/components/GridLayout.tsx new file mode 100644 index 000000000..d6e6c5001 --- /dev/null +++ b/packages/falcon-ui/src/components/GridLayout.tsx @@ -0,0 +1,6 @@ +import { themed } from '../theme'; + +export const GridLayout = themed({ + themeKey: 'gridLayout', + as: 'div' +})(); diff --git a/packages/falcon-ui/src/components/Root.tsx b/packages/falcon-ui/src/components/Root.tsx new file mode 100644 index 000000000..09fed9535 --- /dev/null +++ b/packages/falcon-ui/src/components/Root.tsx @@ -0,0 +1,11 @@ +import { themed } from '../theme'; + +export const Root = themed({ + themeKey: 'root', + as: 'div' +})({ + '*': { + boxSizing: 'border-box', + margin: 0 + } +}); diff --git a/packages/falcon-ui/src/components/ThemeProvider.tsx b/packages/falcon-ui/src/components/ThemeProvider.tsx new file mode 100644 index 000000000..7f3322a74 --- /dev/null +++ b/packages/falcon-ui/src/components/ThemeProvider.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import Provider from '@emotion/provider'; +import { createTheme, PropsWithTheme } from '../theme'; +import { Root } from './Root'; + +const defaultTheme = createTheme(); + +export const ThemeProvider = (props: Partial<PropsWithTheme>) => { + // create default theme if nothing is provided + const themeToUse = props.theme || defaultTheme; + + return ( + <Provider theme={themeToUse}> + <Root {...props} /> + </Provider> + ); +}; diff --git a/packages/falcon-ui/src/components/index.ts b/packages/falcon-ui/src/components/index.ts new file mode 100644 index 000000000..856e823a5 --- /dev/null +++ b/packages/falcon-ui/src/components/index.ts @@ -0,0 +1,6 @@ +export { Button } from './Button'; +export { GridLayout } from './GridLayout'; +export { FlexLayout } from './FlexLayout'; +export { Root } from './Root'; +export { Card } from './Card'; +export { ThemeProvider } from './ThemeProvider'; diff --git a/packages/falcon-ui/src/docs/PropsTable.tsx b/packages/falcon-ui/src/docs/PropsTable.tsx new file mode 100644 index 000000000..648e21f2f --- /dev/null +++ b/packages/falcon-ui/src/docs/PropsTable.tsx @@ -0,0 +1,167 @@ +import * as React from 'react'; +import { Fragment, SFC, ComponentType } from 'react'; +import { withMDXComponents } from '@mdx-js/tag/dist/mdx-provider'; +import { withCSSContext } from '@emotion/core'; +import { mappings } from '../theme/propsmapings'; + +export interface EnumValue { + value: string; + computed: boolean; +} + +export interface FlowTypeElement { + name: string; + value: string; +} + +export interface FlowTypeArgs { + name: string; + type: { + name: string; + }; +} + +export interface PropType { + name: string; + value?: any; + raw?: any; +} + +export interface FlowType extends PropType { + elements: FlowTypeElement[]; + name: string; + raw: string; + type?: string; + signature?: { + arguments: FlowTypeArgs[]; + return: { + name: string; + }; + }; +} + +export interface Prop { + required: boolean; + description?: string; + type: PropType; + defaultValue?: { + value: string; + computed: boolean; + }; + flowType?: FlowType; +} + +export type ComponentWithDocGenInfo = ComponentType & { + __docgenInfo: { + description?: string; + props?: Record<string, Prop>; + }; +}; + +export interface PropsTableProps { + of: ComponentWithDocGenInfo; + components: { + [key: string]: ComponentType<any>; + }; +} + +export type TooltipComponent = React.ComponentType<{ + text: React.ReactNode; + children: React.ReactNode; +}>; + +const getPropType = (prop: Prop, Tooltip?: TooltipComponent) => { + const propName = prop.flowType ? prop.flowType.name : prop.type.name; + const isEnum = false; + const name = isEnum ? 'enum' : propName; + const value = prop.type && prop.type.value; + + if (!name) return null; + if (!Tooltip) return name; + if (!prop.flowType && !isEnum && !value) return name; + if (prop.flowType && !prop.flowType.elements) return name; + + return prop.flowType ? <Tooltip text={prop.flowType}>{name}</Tooltip> : <Tooltip text={prop.type}>{name}</Tooltip>; +}; + +const BasePropsTable: SFC<PropsTableProps> = (props: any) => { + const info = props.of.__docgenInfo; + const components = props.components; + const componentProps = info && info.props; + const defaultProps = props.of.defaultProps; + if (!info || !componentProps) { + console.log('notingf', props); + return null; + } + + const Table = components.table || 'table'; + const Thead = components.thead || 'thead'; + const Tr = components.tr || 'tr'; + const Th = components.th || 'th'; + const Tbody = components.tbody || 'tbody'; + const Td = components.td || 'td'; + const Tooltip = components.tooltip; + + return ( + <Fragment> + <Table className="PropsTable"> + <Thead> + <Tr> + <Th className="PropsTable--property">Property</Th> + <Th className="PropsTable--type">Type</Th> + <Th className="PropsTable--required">Required</Th> + <Th className="PropsTable--default">Default</Th> + <Th className="PropsTable--default">Themed</Th> + <Th width="40%" className="PropsTable--description"> + Description + </Th> + </Tr> + </Thead> + <Tbody> + {componentProps && + Object.keys(componentProps).map((name: string) => { + const prop = componentProps[name]; + const themeKey = defaultProps.themeKey; + const themedValue = props.theme.components[themeKey] ? props.theme.components[themeKey][name] : ''; + + // const cssValue = props.theme[] + // if (props.theme.components[themeKey]) { + + // } + + if (!prop.flowType && !prop.type) return null; + // TODO: uncomment + if (!themedValue) return null; + const mapping = (mappings as any)[name]; + + const cssValue = mapping && mapping.themeProp ? (props.theme as any)[mapping.themeProp] : ''; + // console.log(mapping && mapping.themeProp); + const cssStyle = + mapping && mapping.themeProp === 'colors' + ? { backgroundColor: cssValue[themedValue], display: 'inline-block', height: '20px', width: '20px' } + : {}; + return ( + <Tr key={name}> + <Td>{name}</Td> + <Td>{getPropType(prop, Tooltip)}</Td> + <Td>{String(prop.required)}</Td> + <Td>{defaultProps[name]}</Td> + <Td> + {themedValue}-<span style={cssStyle} /> {cssValue[themedValue]} + </Td> + <Td>{prop.description && prop.description}</Td> + </Tr> + ); + })} + </Tbody> + </Table> + </Fragment> + ); +}; + +export const PropsTable = withMDXComponents( + withCSSContext((props: any, context: any) => { + // console.log(props, context); + return <BasePropsTable {...props} theme={context.theme} />; + }) +); diff --git a/packages/falcon-ui/src/docs/Wrapper.tsx b/packages/falcon-ui/src/docs/Wrapper.tsx new file mode 100644 index 000000000..8d7c45267 --- /dev/null +++ b/packages/falcon-ui/src/docs/Wrapper.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { ThemeProvider } from '../components'; + +const provider = (props: any) => <ThemeProvider {...props} />; + +export default provider; diff --git a/packages/falcon-ui/src/index.mdx b/packages/falcon-ui/src/index.mdx new file mode 100644 index 000000000..80983d45e --- /dev/null +++ b/packages/falcon-ui/src/index.mdx @@ -0,0 +1,9 @@ +--- +name: Getting Started +route: / +order: 1 +--- + +# Getting Started + +TODO \ No newline at end of file diff --git a/packages/falcon-ui/src/index.ts b/packages/falcon-ui/src/index.ts new file mode 100644 index 000000000..7f6410404 --- /dev/null +++ b/packages/falcon-ui/src/index.ts @@ -0,0 +1,2 @@ +export * from './components'; +export * from './theme'; diff --git a/packages/falcon-ui/src/theme/components.ts b/packages/falcon-ui/src/theme/components.ts new file mode 100644 index 000000000..d165f1a6b --- /dev/null +++ b/packages/falcon-ui/src/theme/components.ts @@ -0,0 +1,59 @@ +import { ThemedComponents, ThemedComponent } from './'; + +const button: ThemedComponent = { + color: 'secondaryText', + bg: 'secondary', + p: 'sm', + textAlign: 'center', + borderRadius: 'sm', + boxShadow: 'sm', + fontSize: 'md', + css: props => ({ + transition: 'transform', + transitionTimingFunction: props.theme.easingFunctions.easeIn, + transitionDuration: props.theme.transitionDurations.short, + ':active': { + transform: 'scale(0.9)' + }, + ':hover': { + backgroundColor: props.theme.colors.secondaryLight + } + }), + variants: { + primary: { + borderRadius: 'none' + } + } +}; + +const root: ThemedComponent = { + fontFamily: 'sans', + fontSize: 'sm', + lineHeight: 'default' +}; + +const gridLayout: ThemedComponent = { + display: 'grid', + gridGap: 'sm' +}; + +const flexLayout: ThemedComponent = { + display: 'flex' +}; + +const card: ThemedComponent = { + display: 'block', + p: 'md', + pt: 'lg', + pb: 'lg', + boxShadow: 'sm', + border: 'light' +}; + +export const components: ThemedComponents = { + root, + button, + gridLayout, + flexLayout, + card +}; diff --git a/packages/falcon-ui/src/theme/index.ts b/packages/falcon-ui/src/theme/index.ts new file mode 100644 index 000000000..da458e22f --- /dev/null +++ b/packages/falcon-ui/src/theme/index.ts @@ -0,0 +1,132 @@ +import CSS from 'csstype'; +import merge from 'deepmerge'; + +import { theme } from './theme'; +import { components } from './components'; +import { PropsMappings } from './propsmapings'; + +const defaultTheme: Theme = { + ...theme, + components +}; + +export function createTheme(themeOverride: RecursivePartial<Theme> = {}): Theme { + return merge<Theme, RecursivePartial<Theme>>(defaultTheme, themeOverride); +} + +// export themed component factory +export * from './themed'; + +// --- exported type definitions for theme ---- +export interface Theme { + colors: ThemeColors; + breakpoints: ThemeBreakpoints; + spacing: ThemeSpacing; + fonts: ThemeFonts; + fontSizes: ThemeFontSizes; + fontWeights: ThemeFontWeights; + lineHeights: ThemeLineHeights; + letterSpacings: ThemeLetterSpacings; + borders: ThemeBorders; + borderRadius: ThemeBorderRadius; + boxShadows: ThemeBoxShadows; + easingFunctions: ThemeEasingFunctions; + transitionDurations: ThemeTransitionDurations; + zIndex: ThemeZIndex; + components: ThemedComponents; +} + +type ThemedPropMapping = { + themeProp: keyof Theme; +}; + +type CssPropsKeys = keyof CSS.PropertiesFallback<number | string>; +type CssProps = CSS.PropertiesFallback<number | string>; + +type ResponsivePropMapping = { + cssProp: CssPropsKeys; +}; + +type RecursivePartial<T> = { [P in keyof T]?: RecursivePartial<T[P]> }; + +type CSSPseudoObject = { [K in CSS.SimplePseudos]?: CSSObject }; + +type CssOtherProps = undefined | string | number | CSSObject; + +type CSSOthersObject = { + [propertiesName: string]: CssOtherProps | CssOtherProps[]; +}; + +export interface CSSObject extends CssProps, CSSPseudoObject, CSSOthersObject {} + +export type PropsWithTheme = { theme: Theme }; + +export type InlineCss = ((props: PropsWithTheme) => CSSObject) | CSSObject; + +export type ThemedComponentProps = { + [ComponentProp in keyof PropsMappings]?: + | (PropsMappings[ComponentProp] extends ThemedPropMapping + ? Extract<keyof Theme[PropsMappings[ComponentProp]['themeProp']], string> + : PropsMappings[ComponentProp] extends ResponsivePropMapping + ? CssProps[PropsMappings[ComponentProp]['cssProp']] + : (string | number)) + | { + [Breakpoint in keyof Theme['breakpoints']]?: PropsMappings[ComponentProp] extends ThemedPropMapping + ? Extract<keyof Theme[PropsMappings[ComponentProp]['themeProp']], string> + : PropsMappings[ComponentProp] extends ResponsivePropMapping + ? CssProps[PropsMappings[ComponentProp]['cssProp']] + : (string | number) + } +} & { css?: InlineCss }; + +export type ThemedComponent = ThemedComponentProps & { + variants?: { + [variantKey: string]: ThemedComponentProps; + }; +}; + +export interface ThemedComponents { + [themeKey: string]: ThemedComponent; +} + +type Colors = typeof theme.colors; +export interface ThemeColors extends Colors {} + +type Breakpoints = typeof theme.breakpoints; +export interface ThemeBreakpoints extends Breakpoints {} + +type Spacing = typeof theme.spacing; +export interface ThemeSpacing extends Spacing {} + +type Fonts = typeof theme.fonts; +export interface ThemeFonts extends Fonts {} + +type FontSizes = typeof theme.fontSizes; +export interface ThemeFontSizes extends FontSizes {} + +type FontWeights = typeof theme.fontWeights; +export interface ThemeFontWeights extends FontWeights {} + +type LineHeights = typeof theme.lineHeights; +export interface ThemeLineHeights extends LineHeights {} + +type LetterSpacings = typeof theme.letterSpacings; +export interface ThemeLetterSpacings extends LetterSpacings {} + +type Borders = typeof theme.borders; +export interface ThemeBorders extends Borders {} + +type BorderRadius = typeof theme.borderRadius; +export interface ThemeBorderRadius extends BorderRadius {} + +type BoxShadows = typeof theme.boxShadows; +export interface ThemeBoxShadows extends BoxShadows {} + +type EasingFunctions = typeof theme.easingFunctions; +export interface ThemeEasingFunctions extends EasingFunctions {} + +type TransitionDurations = typeof theme.transitionDurations; +export interface ThemeTransitionDurations extends TransitionDurations {} + +type ZIndex = typeof theme.zIndex; +export interface ThemeZIndex extends ZIndex {} diff --git a/packages/falcon-ui/src/theme/propsmapings.ts b/packages/falcon-ui/src/theme/propsmapings.ts new file mode 100644 index 000000000..12b5c66ed --- /dev/null +++ b/packages/falcon-ui/src/theme/propsmapings.ts @@ -0,0 +1,150 @@ +import CSS from 'csstype'; +import { Theme } from './'; + +function propsMapping<T extends PropsMapping>(param: T) { + return param; +} + +export const mappings = propsMapping({ + /** + * Themed margin + */ + m: { + cssProp: 'margin', + themeProp: 'spacing' + }, + mt: { + cssProp: 'marginTop', + themeProp: 'spacing' + }, + ml: { + cssProp: 'marginLeft', + themeProp: 'spacing' + }, + mr: { + cssProp: 'marginRight', + themeProp: 'spacing' + }, + mb: { + cssProp: 'marginBottom', + themeProp: 'spacing' + }, + p: { + cssProp: 'padding', + themeProp: 'spacing' + }, + pt: { + cssProp: 'paddingTop', + themeProp: 'spacing' + }, + pl: { + cssProp: 'paddingLeft', + themeProp: 'spacing' + }, + pr: { + cssProp: 'paddingRight', + themeProp: 'spacing' + }, + pb: { + cssProp: 'paddingBottom', + themeProp: 'spacing' + }, + bg: { + cssProp: 'backgroundColor', + themeProp: 'colors' + }, + color: { + cssProp: 'color', + themeProp: 'colors' + }, + + width: {}, + fontSize: { + themeProp: 'fontSizes' + }, + fontFamily: { + themeProp: 'fonts' + }, + textAlign: {}, + lineHeight: { + themeProp: 'lineHeights' + }, + fontWeight: { + themeProp: 'fontWeights' + }, + letterSpacing: { + themeProp: 'letterSpacings' + }, + display: {}, + + alignItems: {}, + justifyContent: {}, + flexWrap: {}, + flexDirection: {}, + flex: {}, + alignContent: {}, + justifySelf: {}, + alignSelf: {}, + order: {}, + flexBasis: {}, + gridGap: { + themeProp: 'spacing' + }, + gridRowGap: { + themeProp: 'spacing' + }, + gridColumnGap: { + themeProp: 'spacing' + }, + gridColumn: {}, + gridRow: {}, + gridAutoFlow: {}, + gridAutoRows: {}, + gridAutoColumns: {}, + gridTemplateRows: {}, + gridTemplateColumns: {}, + borderRadius: { + themeProp: 'borderRadius' + }, + borderColor: { + themeProp: 'colors' + }, + border: { + themeProp: 'borders' + }, + borderTop: { + themeProp: 'borders' + }, + borderRight: { + themeProp: 'borders' + }, + borderBottom: { + themeProp: 'borders' + }, + borderLeft: { + themeProp: 'borders' + }, + boxShadow: { + themeProp: 'boxShadows' + }, + opacity: {}, + position: {}, + top: {}, + right: {}, + bottom: {}, + left: {}, + zIndex: { + themeProp: 'zIndex' + } +}); + +export type PropsMappings = typeof mappings; + +export type ResponsivePropMapping = { + cssProp?: keyof CSS.Properties; + themeProp?: keyof Theme; +}; + +type PropsMapping = { + [name: string]: ResponsivePropMapping; +}; diff --git a/packages/falcon-ui/src/theme/theme.ts b/packages/falcon-ui/src/theme/theme.ts new file mode 100644 index 000000000..e2f11d3c0 --- /dev/null +++ b/packages/falcon-ui/src/theme/theme.ts @@ -0,0 +1,105 @@ +export const theme = { + colors: { + primary: '#eeeeee', + primaryLight: '#ffffff', + primaryDark: '##bcbcbc', + primaryText: '#000000', + + secondary: '#01579b', + secondaryLight: '#4f83cc', + secondaryDark: '#002f6c', + secondaryText: '#ffffff', + + error: '#f44336', + errorText: '#000000', + black: '#000000', + white: '#ffffff' + }, + + breakpoints: { + xs: 0, + sm: 600, + md: 960, + lg: 1280, + xl: 1920 + }, + + spacing: { + none: 0, + xs: 4, + sm: 8, + md: 16, + lg: 32, + xl: 64 + }, + + fonts: { + sans: '"Segoe UI", system-ui, sans-serif', + mono: '"SF Mono", "Roboto Mono", Menlo, monospace' + }, + + fontSizes: { + xs: 12, + sm: 14, + md: 16, + lg: 20, + xl: 24, + xxl: 34, + xxxl: 48 + }, + + fontWeights: { + light: 300, + regular: 400, + bold: 700 + }, + + lineHeights: { + default: 1.3 + }, + + letterSpacings: { + normal: 'normal', + caps: '0.025em' + }, + + borders: { + light: '0.5px', + regular: '1px' + }, + + borderRadius: { + none: 0, + xs: 4, + sm: 8, + md: 16, + lg: 32, + xl: '100%' + }, + + boxShadows: { + none: 'none', + xs: '0 0 2px 0 rgba(0,0,0,.08),0 1px 4px 0 rgba(0,0,0,.16)', + sm: '0 0 2px 0 rgba(0,0,0,.08),0 2px 8px 0 rgba(0,0,0,.16)', + md: '0 0 2px 0 rgba(0,0,0,.08),0 4px 16px 0 rgba(0,0,0,.16)', + lg: '0 0 2px 0 rgba(0,0,0,.08),0 8px 32px 0 rgba(0,0,0,.16)' + }, + + easingFunctions: { + easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)', + easeOut: 'cubic-bezier(0.0, 0, 0.2, 1)', + easeIn: 'cubic-bezier(0.4, 0, 1, 1)', + sharp: 'cubic-bezier(0.4, 0, 0.6, 1)' + }, + + transitionDurations: { + short: '150ms', + standard: '250ms', + long: '375ms' + }, + + zIndex: { + modal: 1000, + tooltip: 1500 + } +}; diff --git a/packages/falcon-ui/src/theme/themed.tsx b/packages/falcon-ui/src/theme/themed.tsx new file mode 100644 index 000000000..2187bda1d --- /dev/null +++ b/packages/falcon-ui/src/theme/themed.tsx @@ -0,0 +1,184 @@ +import React from 'react'; +import styled from '@emotion/styled-base'; +import isPropValid from '@emotion/is-prop-valid'; +import { Theme, CSSObject, PropsWithTheme, ThemedComponentProps } from './'; +import { mappings, PropsMappings, ResponsivePropMapping } from './propsmapings'; + +const propsMappingKeys = Object.keys(mappings) as (keyof PropsMappings)[]; + +const convertPropToCss = ( + mappingKey: keyof PropsMappings, + propMapping: ResponsivePropMapping, + matchingProp: string | number, + theme: Theme +) => { + // if mapping does not have cssProp specified fallback to it's key as css property name + const cssPropName = propMapping.cssProp || mappingKey; + // if matching props is themable prop then get it's actual value from theme props otherwise + // then just pass it as css prop value + // TODO: typescript: is there a way to improve those typings? + const cssPropValue = !propMapping.themeProp ? matchingProp : (theme[propMapping.themeProp] as any)[matchingProp]; + + return { + cssPropName, + cssPropValue + }; +}; + +type ThemedBreakpointsKeysType = keyof Theme['breakpoints']; +type ThemedProps = ThemedComponentProps & PropsWithTheme; + +const convertThemedPropsToCss = (props: ThemedProps): CSSObject => { + // if theme is not provided via theme provider do not map anything + if (!props.theme) { + return {}; + } + const responsiveBreakpoints = Object.keys(props.theme.breakpoints) as (ThemedBreakpointsKeysType)[]; + // TODO: typescript: can typings be improved for that object? + const cssObject = {} as any; + // iterate over all possible responsive props keys and check if passed props have matching prop + // this is hot path function called potentially many times + for (let i = 0; i < propsMappingKeys.length; i++) { + const mappingKey = propsMappingKeys[i]; + const matchingProp = props[mappingKey]; + const propMapping: ResponsivePropMapping = mappings[mappingKey]; + + // move along if there is no matching prop for given key found + if (!matchingProp) { + continue; + } + + // if matching prop is typeof string it means it's not responsive + if (typeof matchingProp === 'string' || typeof matchingProp === 'number') { + const cssPair = convertPropToCss(mappingKey, propMapping, matchingProp, props.theme); + cssObject[cssPair.cssPropName] = cssPair.cssPropValue; + } else { + // if it's not string it needs to be object that has responsive breakpoints keys + for (let j = 0; j < responsiveBreakpoints.length; j++) { + const breakpointKey = responsiveBreakpoints[j]; + // if matching prop has no matching breakpoint key move along + const matchingResponsiveProp = matchingProp[breakpointKey]; + if (!matchingResponsiveProp) { + continue; + } + + const breakpointValue = props.theme.breakpoints[breakpointKey]; + // if specified breakpoint has value 0 (usually default breakpoint) + // then do not create media query for it, just pass the props straight to the object + if (breakpointValue === 0) { + const cssPair = convertPropToCss(mappingKey, propMapping, matchingResponsiveProp, props.theme); + cssObject[cssPair.cssPropName] = cssPair.cssPropValue; + } else { + // if breakpoint value is different than 0 all css props needs to be inside '@media' object + + const mediaQueryMinWidth = typeof breakpointValue === 'number' ? `${breakpointValue}px` : breakpointValue; + const mediaQueryKey = `@media screen and (min-width: ${mediaQueryMinWidth})`; + // add media query key to css object if it hasn't already got one + if (!cssObject[mediaQueryKey]) { + cssObject[mediaQueryKey] = {}; + } + const cssPair = convertPropToCss(mappingKey, propMapping, matchingResponsiveProp, props.theme); + + cssObject[mediaQueryKey][cssPair.cssPropName] = cssPair.cssPropValue; + } + } + } + } + + return cssObject; +}; + +function getThemedCss(props: ThemedProps & BaseProps) { + // if theme is not provided via theme provider or inline theme prop do return any css + if (!props.theme) { + return; + } + + const componentPropsDefinedInTheme = (props.themeKey && props.theme.components[props.themeKey]) || {}; + const componentVariantPropsDefinedInTheme = + (props.themeKey && + props.variant && + props.theme.components[props.themeKey] && + props.theme.components[props.themeKey]['variants'] && + (props.theme.components[props.themeKey]['variants'] as any)[props.variant]) || + {}; + // responsive props get merged together and then converted to Css object here + // order or merging is important + const cssFromThemeAndProps = convertThemedPropsToCss({ + ...componentPropsDefinedInTheme, // we start with component props defined with theme + ...componentVariantPropsDefinedInTheme, // then merge those with props defined in variant if variant is specified + ...props // finally merge any defined props directly on component + } as ThemedProps); + + // css props defined via css prop are merged as well + // each of css props no matter if defined as prop on component or in theme or in theme variant + // can be a function so we need to execute it using props we have here + const cssPropDefinedInTheme = + typeof componentPropsDefinedInTheme.css === 'function' + ? componentPropsDefinedInTheme.css(props) + : componentPropsDefinedInTheme.css || {}; + + const cssPropDefinedInThemeVariant = + typeof componentVariantPropsDefinedInTheme.css === 'function' + ? componentVariantPropsDefinedInTheme.css(props) + : componentVariantPropsDefinedInTheme.css || {}; + + const cssPropDefinedInlineOnComponent = typeof props.css === 'function' ? props.css(props) : props.css || {}; + + const cssFromInlineCssProps = { + ...cssPropDefinedInTheme, // start with css prop defined in theme + ...cssPropDefinedInThemeVariant, // then merge it with css prop defined in theme variant + ...cssPropDefinedInlineOnComponent // and finally merge it with css prop defined directly on component + }; + + return { ...cssFromThemeAndProps, ...cssFromInlineCssProps }; +} + +type BaseProps = { + as: React.ReactType; + themeKey?: string; + variant?: string; +}; + +// this component handles dynamic html tag rendering via as prop as well as forwards ref to DOM element +const Tag = React.forwardRef<{}, { color: any; as: any }>(({ as: Component, color, ...props }, ref) => ( + <Component {...props} ref={ref} /> +)); + +export function themed<T = {}>(defaultProps: Exclude<T, BaseProps> & BaseProps) { + type ComponentProps = Partial<typeof defaultProps> & Partial<ThemedProps> & { [x: string]: any }; + type DefaultInlineCss = ((props: ComponentProps) => CSSObject) | CSSObject; + + // themed returns function that accepts default css to be provided by the component + // it accepts any number or DefaultInlineCss args + return (...css: DefaultInlineCss[]) => { + // when custom component is provided via 'as' prop then use it for rendering + // otherwise render using our generic 'Tag' component + const shouldRenderCustomComponent = typeof defaultProps.as === 'function'; + const tagToRender = shouldRenderCustomComponent ? defaultProps.as : Tag; + + const ThemedComponent: React.SFC<ComponentProps> = styled(tagToRender, { + // that method ensures that no custom props are rendered in output html + // 1 forward props that aren't in our responsive props mapping + // 2 forward only valid html props or prop 'as' when there is no custom compoent provided + // as we need 'as' prop in our Tag component + shouldForwardProp: (name: string) => + propsMappingKeys.indexOf(name as any) === -1 && + (isPropValid(name) || (!shouldRenderCustomComponent && name === 'as')), + + label: `${defaultProps.themeKey}${defaultProps.variant ? `-${defaultProps.variant}` : ''}` + })(...css, getThemedCss); + + ThemedComponent.defaultProps = defaultProps; + + return ThemedComponent; + }; +} + +// IMPORTANT TODO: typescript: +// Themed factory could be smarter +// when it comes to providing property types based on provided 'as' tag or Component, unfortunatelly it's currently +// blocked by typescript issue: https://github.com/Microsoft/TypeScript/issues/26004 +// similar issue: https://github.com/mui-org/material-ui/pull/11731 +// partial hack that does not work for our use case +// https://stackoverflow.com/questions/51183228/typescript-parameter-type-inference-failure diff --git a/packages/falcon-ui/tsconfig.json b/packages/falcon-ui/tsconfig.json new file mode 100644 index 000000000..8b0e8e0e6 --- /dev/null +++ b/packages/falcon-ui/tsconfig.json @@ -0,0 +1,22 @@ +{ + "include": ["src", "types"], + "compilerOptions": { + "rootDir": "src", + "allowSyntheticDefaultImports": true, + "module": "esnext", + "moduleResolution": "node", + "jsx": "react", + "pretty": true, + "strict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUnusedParameters": true, + "esModuleInterop": true, + "sourceMap": true, + "emitDeclarationOnly": true, + "declaration": true, + "skipLibCheck": true, + "declarationDir": "dist", + "forceConsistentCasingInFileNames": true + } +} diff --git a/packages/falcon-ui/types/types.d.ts b/packages/falcon-ui/types/types.d.ts new file mode 100644 index 000000000..35ded5d74 --- /dev/null +++ b/packages/falcon-ui/types/types.d.ts @@ -0,0 +1,6 @@ +declare module '@emotion/core'; +declare module '@emotion/provider'; +declare module '@emotion/is-prop-valid'; +declare module '@emotion/styled-base'; +declare module '@mdx-js/tag'; +declare module '@mdx-js/tag/dist/mdx-provider';