From 66156551a506473b6c2d4c6e87c5e7537cc887b8 Mon Sep 17 00:00:00 2001 From: codebender828 Date: Sun, 1 Dec 2019 21:18:12 +0800 Subject: [PATCH] refactor(kiwi): rollup builds and lerna workspaces --- .gitignore | 7 +- lerna.json | 15 + package.json | 67 +- packages/kiwi-ui/package.json | 25 + packages/kiwi-ui/rollup.config.js | 99 + packages/kiwi-ui/src/Alert/alert.styles.js | 150 + packages/kiwi-ui/src/Alert/index.d.ts | 24 + packages/kiwi-ui/src/Alert/index.js | 112 + packages/kiwi-ui/src/Badge/badge.styles.js | 69 + packages/kiwi-ui/src/Badge/badge.test.js | 21 + packages/kiwi-ui/src/Badge/index.d.ts | 19 + packages/kiwi-ui/src/Badge/index.js | 46 + packages/kiwi-ui/src/Box/index.js | 77 + packages/kiwi-ui/src/Button/button.props.js | 69 + packages/kiwi-ui/src/Button/button.styles.js | 264 + packages/kiwi-ui/src/Button/button.test.js | 20 + packages/kiwi-ui/src/Button/index.d.ts | 20 + packages/kiwi-ui/src/Button/index.js | 124 + packages/kiwi-ui/src/Button/styles.js | 230 + packages/kiwi-ui/src/CloseButton/index.d.ts | 29 + packages/kiwi-ui/src/CloseButton/index.js | 103 + packages/kiwi-ui/src/Icon/icon.utils.js | 26 + packages/kiwi-ui/src/Icon/index.d.ts | 13 + packages/kiwi-ui/src/Icon/index.js | 81 + .../src/IconButton/icon-button.utils.js | 37 + packages/kiwi-ui/src/IconButton/index.d.ts | 12 + packages/kiwi-ui/src/IconButton/index.js | 77 + packages/kiwi-ui/src/PseudoBox/index.js | 19 + packages/kiwi-ui/src/PseudoBox/utils.js | 50 + packages/kiwi-ui/src/README.md | 39 + packages/kiwi-ui/src/Spinner/index.d.ts | 16 + packages/kiwi-ui/src/Spinner/index.js | 97 + packages/kiwi-ui/src/Text/index.d.ts | 10 + packages/kiwi-ui/src/Text/index.js | 23 + packages/kiwi-ui/src/ThemeProvider/index.d.ts | 5 + packages/kiwi-ui/src/ThemeProvider/index.js | 29 + .../kiwi-ui/src/ThemeProvider/index.test.js | 34 + packages/kiwi-ui/src/Toast/index.d.ts | 40 + packages/kiwi-ui/src/Toast/index.js | 161 + .../kiwi-ui/src/TransitionExpand/index.js | 91 + packages/kiwi-ui/src/VisuallyHidden/index.js | 16 + packages/kiwi-ui/src/babel.config.js | 14 + packages/kiwi-ui/src/config/props/index.js | 18 + .../kiwi-ui/src/config/props/props.config.js | 165 + packages/kiwi-ui/src/config/props/props.js | 123 + .../kiwi-ui/src/config/props/pseudo.config.js | 0 packages/kiwi-ui/src/config/props/pseudo.js | 28 + packages/kiwi-ui/src/index.js | 16 + packages/kiwi-ui/src/lib/internal-icons.js | 86 + packages/kiwi-ui/src/plugin/index.js | 30 + packages/kiwi-ui/src/utils/color.js | 55 + packages/kiwi-ui/src/utils/icons.js | 35 + packages/kiwi-ui/src/utils/index.js | 6 + packages/kiwi-ui/src/utils/logger.js | 61 + packages/kiwi-ui/src/utils/object.js | 19 + packages/kiwi-ui/src/utils/provide-theme.js | 10 + packages/kiwi-ui/src/utils/transform.js | 46 + src/App.vue | 12 +- yarn.lock | 6375 ++++++++++++----- 59 files changed, 7625 insertions(+), 1940 deletions(-) create mode 100644 lerna.json create mode 100644 packages/kiwi-ui/package.json create mode 100644 packages/kiwi-ui/rollup.config.js create mode 100644 packages/kiwi-ui/src/Alert/alert.styles.js create mode 100644 packages/kiwi-ui/src/Alert/index.d.ts create mode 100644 packages/kiwi-ui/src/Alert/index.js create mode 100644 packages/kiwi-ui/src/Badge/badge.styles.js create mode 100644 packages/kiwi-ui/src/Badge/badge.test.js create mode 100644 packages/kiwi-ui/src/Badge/index.d.ts create mode 100644 packages/kiwi-ui/src/Badge/index.js create mode 100644 packages/kiwi-ui/src/Box/index.js create mode 100644 packages/kiwi-ui/src/Button/button.props.js create mode 100644 packages/kiwi-ui/src/Button/button.styles.js create mode 100644 packages/kiwi-ui/src/Button/button.test.js create mode 100644 packages/kiwi-ui/src/Button/index.d.ts create mode 100644 packages/kiwi-ui/src/Button/index.js create mode 100644 packages/kiwi-ui/src/Button/styles.js create mode 100644 packages/kiwi-ui/src/CloseButton/index.d.ts create mode 100644 packages/kiwi-ui/src/CloseButton/index.js create mode 100644 packages/kiwi-ui/src/Icon/icon.utils.js create mode 100644 packages/kiwi-ui/src/Icon/index.d.ts create mode 100644 packages/kiwi-ui/src/Icon/index.js create mode 100644 packages/kiwi-ui/src/IconButton/icon-button.utils.js create mode 100644 packages/kiwi-ui/src/IconButton/index.d.ts create mode 100644 packages/kiwi-ui/src/IconButton/index.js create mode 100644 packages/kiwi-ui/src/PseudoBox/index.js create mode 100644 packages/kiwi-ui/src/PseudoBox/utils.js create mode 100644 packages/kiwi-ui/src/README.md create mode 100644 packages/kiwi-ui/src/Spinner/index.d.ts create mode 100644 packages/kiwi-ui/src/Spinner/index.js create mode 100644 packages/kiwi-ui/src/Text/index.d.ts create mode 100644 packages/kiwi-ui/src/Text/index.js create mode 100644 packages/kiwi-ui/src/ThemeProvider/index.d.ts create mode 100644 packages/kiwi-ui/src/ThemeProvider/index.js create mode 100644 packages/kiwi-ui/src/ThemeProvider/index.test.js create mode 100644 packages/kiwi-ui/src/Toast/index.d.ts create mode 100644 packages/kiwi-ui/src/Toast/index.js create mode 100644 packages/kiwi-ui/src/TransitionExpand/index.js create mode 100644 packages/kiwi-ui/src/VisuallyHidden/index.js create mode 100644 packages/kiwi-ui/src/babel.config.js create mode 100644 packages/kiwi-ui/src/config/props/index.js create mode 100644 packages/kiwi-ui/src/config/props/props.config.js create mode 100644 packages/kiwi-ui/src/config/props/props.js create mode 100644 packages/kiwi-ui/src/config/props/pseudo.config.js create mode 100644 packages/kiwi-ui/src/config/props/pseudo.js create mode 100644 packages/kiwi-ui/src/index.js create mode 100644 packages/kiwi-ui/src/lib/internal-icons.js create mode 100644 packages/kiwi-ui/src/plugin/index.js create mode 100644 packages/kiwi-ui/src/utils/color.js create mode 100644 packages/kiwi-ui/src/utils/icons.js create mode 100644 packages/kiwi-ui/src/utils/index.js create mode 100644 packages/kiwi-ui/src/utils/logger.js create mode 100644 packages/kiwi-ui/src/utils/object.js create mode 100644 packages/kiwi-ui/src/utils/provide-theme.js create mode 100644 packages/kiwi-ui/src/utils/transform.js diff --git a/.gitignore b/.gitignore index efda7bee..c699314e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .DS_Store node_modules -/dist +*/dist/* /public /storybook-static @@ -24,3 +24,8 @@ yarn-error.log* *.njsproj *.sln *.sw? + +# Workspaces +packages/*/coverage +packages/*/dist +packages/*/node_modules \ No newline at end of file diff --git a/lerna.json b/lerna.json new file mode 100644 index 00000000..293fb22a --- /dev/null +++ b/lerna.json @@ -0,0 +1,15 @@ +{ + "packages": [ + "packages/*" + ], + "npmClient": "yarn", + "version": "independent", + "useWorkspaces": true, + "registry": "https://registry.npmjs.org/", + "command": { + "publish": { + "conventionalCommits": true, + "message": "chore(release): publish" + } + } +} diff --git a/package.json b/package.json index 5308251e..9661ad9e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,9 @@ { - "name": "kiwi-ui", - "version": "0.1.0", + "name": "root", "private": true, + "workspaces": [ + "packages/*" + ], "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", @@ -12,61 +14,74 @@ "build-storybook": "build-storybook --quiet", "docs:dev": "vuepress dev docs", "docs:build": "vuepress build docs", - "build-vsc": "cd node_modules/vue-styled-components && npm install && npm run build && cd ../../" - }, - "dependencies": { - "@babel/preset-env": "^7.6.3", - "@fortawesome/free-solid-svg-icons": "^5.11.2", - "@fortawesome/pro-light-svg-icons": "^5.11.2", - "@styled-system/should-forward-prop": "^5.1.2", - "@vue/composition-api": "^0.3.2", - "breadstick": "^0.1.17", - "color": "^3.1.2", - "core-js": "^2.6.5", - "css-loader": "^3.2.0", - "esm": "^3.2.25", - "lodash-es": "^4.17.15", - "register-service-worker": "^1.6.2", - "style-loader": "^1.0.0", - "velocity-animate": "^1.5.2", - "vue": "^2.6.10", - "vue-create-context": "^1.1.0", - "vue-notification": "^1.3.20", - "vue-router": "^3.0.3", - "vue-styled-components": "git+https://github.com/codebender828/vue-styled-components.git" + "bootstrap": "lerna bootstrap --use-workspaces" }, "devDependencies": { - "@babel/core": "^7.6.0", - "@babel/plugin-transform-modules-commonjs": "^7.6.0", + "@babel/core": "^7.7.4", + "@babel/plugin-proposal-object-rest-spread": "^7.7.4", + "@babel/plugin-transform-modules-commonjs": "^7.7.4", + "@babel/plugin-transform-parameters": "^7.7.4", + "@babel/preset-env": "^7.7.4", "@commitlint/cli": "^8.2.0", "@commitlint/config-conventional": "^8.2.0", + "@fortawesome/free-solid-svg-icons": "^5.11.2", + "@fortawesome/pro-light-svg-icons": "^5.11.2", "@storybook/addon-actions": "^5.2.1", "@storybook/addon-centered": "^5.2.1", "@storybook/addon-links": "^5.2.1", "@storybook/addons": "^5.2.1", "@storybook/vue": "^5.2.1", + "@styled-system/css": "^5.0.23", + "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0", + "@vue/babel-preset-app": "^4.1.1", + "@vue/babel-preset-jsx": "^1.1.2", "@vue/cli-plugin-babel": "^3.11.0", "@vue/cli-plugin-e2e-cypress": "^3.11.0", "@vue/cli-plugin-eslint": "^3.11.0", "@vue/cli-plugin-pwa": "^3.11.0", "@vue/cli-plugin-unit-jest": "^3.11.0", "@vue/cli-service": "^3.11.0", + "@vue/composition-api": "^0.3.2", "@vue/eslint-config-standard": "^4.0.0", "@vue/test-utils": "1.0.0-beta.29", "babel-core": "7.0.0-bridge.0", "babel-eslint": "^10.0.1", + "babel-helper-vue-jsx-merge-props": "^2.0.3", "babel-jest": "^23.6.0", "babel-loader": "^8.0.6", + "babel-plugin-syntax-jsx": "^6.18.0", + "babel-plugin-transform-es2015-for-of": "^6.23.0", + "babel-plugin-transform-vue-jsx": "^3.7.0", + "babel-preset-env": "^1.7.0", + "babel-preset-es2015": "^6.24.1", "babel-preset-vue": "^2.0.2", + "babelrc-rollup": "^3.0.0", + "breadstick": "^0.1.17", + "color": "^3.1.2", + "core-js": "^3.4.5", "eslint": "^5.16.0", "eslint-plugin-vue": "^5.0.0", "eslint-plugin-vue-a11y": "^0.0.31", "husky": "^3.0.8", "install": "^0.13.0", + "lerna": "^3.18.5", "lint-staged": "^9.4.2", "node-sass": "^4.12.0", + "register-service-worker": "^1.6.2", + "rollup-plugin-babel": "^4.3.3", + "rollup-plugin-buble": "^0.19.8", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-json": "^4.0.0", + "rollup-plugin-jsx": "^1.0.3", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-terser": "^5.1.2", + "rollup-plugin-vue": "^5.1.4", "sass-loader": "^8.0.0", + "styled-system": "^5.1.2", + "velocity-animate": "^1.5.2", + "vue": "^2.6.10", "vue-loader": "^15.7.1", + "vue-styled-components": "git+https://github.com/codebender828/vsc.git", "vue-template-compiler": "^2.6.10", "vuepress": "^1.2.0" } diff --git a/packages/kiwi-ui/package.json b/packages/kiwi-ui/package.json new file mode 100644 index 00000000..ffebea73 --- /dev/null +++ b/packages/kiwi-ui/package.json @@ -0,0 +1,25 @@ +{ + "name": "@kiwi-ui/core", + "version": "1.0.0", + "description": "🥝 A Scalable, Accessible, High Performance, Vue.js UI component library", + "author": "Jonathan Bakebwa ", + "license": "MIT", + "sideEffects": false, + "private": false, + "scripts": { + "build": "rollup --c --watch" + }, + "dependencies": { + "breadstick": "^0.1.17", + "color": "^3.1.2", + "lodash-es": "^4.17.15" + }, + "peerDependencies": { + "velocity-animate": "^1.5.2", + "vue": "^2.6.10", + "vue-styled-components": "^1.4.14" + }, + "devDependencies": { + "core-js": "^3.4.5" + } +} diff --git a/packages/kiwi-ui/rollup.config.js b/packages/kiwi-ui/rollup.config.js new file mode 100644 index 00000000..431b5547 --- /dev/null +++ b/packages/kiwi-ui/rollup.config.js @@ -0,0 +1,99 @@ +import babel from 'rollup-plugin-babel' +import resolve from 'rollup-plugin-node-resolve' +import cjs from 'rollup-plugin-commonjs' +import { terser } from 'rollup-plugin-terser' +import buble from 'rollup-plugin-buble' +import vue from 'rollup-plugin-vue' +import pkg from './package.json' + +const production = !process.env.ROLLUP_WATCH + +// Plugins +const bubelConfig = buble({ + objectAssign: 'Object.assign', + transforms: { + dangerousTaggedTemplateString: true, + dangerousForOf: true + } +}) + +const babelConfig = babel({ + exclude: /node_modules/, + runtimeHelpers: true, + babelrc: false, + presets: [ + [ + '@babel/preset-env', { + modules: false + } + ] + ], + plugins: [ + 'babel-plugin-transform-es2015-for-of' + ] +}) + +const vueConfig = vue({ + template: { + isProduction: true + } +}) + +// Externals +const externals = [ + ...Object.keys(pkg.peerDependencies || {}) +] + +const commons = { + external: externals, + plugins: [ + resolve(), + bubelConfig, + babelConfig, + vueConfig, + cjs({ + include: /node_modules/ + }), + production && terser() + ] +} + +/** + * Configurations + */ +export default [ + { + input: 'src/index.js', + output: [ + { + file: `dist/index.esm.js`, + format: 'esm' + } + ], + ...commons + }, + { + input: 'src/index.js', + output: [ + { + name: 'KiwiUI', + file: `dist/index.umd.js`, + format: 'umd', + exports: 'named' + } + ], + ...commons + }, + { + input: 'src/index.js', + output: [ + { + name: 'KiwiUI', + file: `dist/index.cjs.js`, + format: 'cjs', + exports: 'named' + } + ], + ...commons + } +] diff --git a/packages/kiwi-ui/src/Alert/alert.styles.js b/packages/kiwi-ui/src/Alert/alert.styles.js new file mode 100644 index 00000000..12b4ee16 --- /dev/null +++ b/packages/kiwi-ui/src/Alert/alert.styles.js @@ -0,0 +1,150 @@ +import { colorEmphasis } from '../utils' + +export const statuses = { + info: { icon: 'info', color: 'blue' }, + warning: { icon: 'warning-2', color: 'orange' }, + success: { icon: 'check-circle', color: 'green' }, + error: { icon: 'warning', color: 'red' } +} + +const baseProps = { + display: 'flex', + alignItems: 'center', + position: 'relative', + overflow: 'hidden', + pl: 4, + pr: 4, + pt: 3, + pb: 3 +} + +/** + * @description Create leftAccent alert styles + * @param {Object} props + * @property {String} color + */ +const leftAccent = props => { + const { color } = props + return { + light: { + pl: 3, + ...subtle(props).light, + borderLeft: '4px', + borderColor: `${color}.500` + }, + dark: { + pl: 3, + ...subtle(props).dark, + borderLeft: '4px', + borderColor: `${color}.200` + } + } +} + +/** + * @description Create topAccent alert styles + * @param {Object} props + * @property {String} color + */ +const topAccent = props => { + const { color } = props + return { + light: { + pt: 2, + ...subtle(props).light, + borderTop: '4px', + borderColor: `${color}.500` + }, + dark: { + pt: 2, + ...subtle(props).dark, + borderTop: '4px', + borderColor: `${color}.200` + } + } +} + +/** + * @description Create solid alert styles + * @param {Object} props + * @property {String} color + */ +const solid = ({ color }) => { + return { + light: { bg: `${color}.500`, color: 'white' }, + dark: { bg: `${color}.200`, color: 'gray.900' } + } +} + +/** + * @description Create subtle alert styles + * @param {Object} props + * @property {String} color + */ +const subtle = ({ color, theme: { colors } }) => { + let darkBg = colors[color] && colors[color][200] + return { + light: { + bg: `${color}.100` + }, + dark: { bg: colorEmphasis(darkBg, 'lowest') } + } +} + +/** + * @description Evaluate variant styles + * @param {Object} props + * @returns {Object} Style props + */ +const statusStyleProps = props => { + switch (props.variant) { + case 'solid': + return solid(props) + case 'subtle': + return subtle(props) + case 'topAccent': + return topAccent(props) + case 'leftAccent': + return leftAccent(props) + default: + return {} + } +} + +/** + * @description Create styles for alert component + * @param {Object} context + * @property {String} variant + * @property {String} color + * @property {String} colorMode + * @property {Object} theme + * @returns {Object} Style props + */ +const useAlertStyle = ({ variant, color, colorMode, theme }) => { + const _props = { variant, color, theme } + return { + ...baseProps, + ...statusStyleProps(_props)[colorMode] + } +} + +/** + * @description Create alert icon styles + * @param {Object} context + * @property {String} variant + * @property {String} colorMode + * @property {String} color + * @returns {Object} Style props + */ +export const useAlertIconStyle = ({ variant, colorMode, color }) => { + if (['left-accent', 'top-accent', 'subtle'].includes(variant)) { + let result = { + light: { color: `${color}.500` }, + dark: { color: `${color}.200` } + } + + return result[colorMode] + } +} + +export default useAlertStyle diff --git a/packages/kiwi-ui/src/Alert/index.d.ts b/packages/kiwi-ui/src/Alert/index.d.ts new file mode 100644 index 00000000..f64f1342 --- /dev/null +++ b/packages/kiwi-ui/src/Alert/index.d.ts @@ -0,0 +1,24 @@ +import Vue from 'vue' + +/** + * The alert component for relaying useful information to the UI + */ +export const Alert: Vue.Component<{ + status?: String, + variant?: String +}>; + +/** + * Icon component for composing alerts + */ +export const AlertIcon: Vue.Component<{}>; + +/** + * Alert title component for composing alert title. + */ +export const AlertTitle: Vue.Component<{}>; + +/** + * Alert description component for composing alert descriptions. + */ +export const AlertDescription: Vue.Component<{}>; diff --git a/packages/kiwi-ui/src/Alert/index.js b/packages/kiwi-ui/src/Alert/index.js new file mode 100644 index 00000000..e04bcacf --- /dev/null +++ b/packages/kiwi-ui/src/Alert/index.js @@ -0,0 +1,112 @@ +import Box from '../Box' +import Icon from '../Icon' +import { baseProps } from '../config/props' +import { forwardProps } from '../utils' +import useAlertStyle, { useAlertIconStyle } from './alert.styles' + +export const statuses = { + info: { icon: '_info', color: 'blue' }, + warning: { icon: '_warning-2', color: 'orange' }, + success: { icon: '_check-circle', color: 'green' }, + error: { icon: '_warning', color: 'red' } +} + +const Alert = { + name: 'Alert', + inject: ['$theme', '$colorMode'], + provide () { + return { + _status: this.status, + _variant: this.variant + } + }, + props: { + status: { + type: [String, Array], + default: 'info' + }, + variant: { + type: [String, Array], + default: 'subtle' + }, + ...baseProps + }, + render (h) { + const alertStyles = useAlertStyle({ + variant: this.variant, + color: statuses[this.status] && statuses[this.status]['color'], + colorMode: this.$colorMode, + theme: this.$theme() + }) + + return h(Box, { + props: { + ...alertStyles, + ...forwardProps(this.$props) + }, + attrs: { + role: 'alert' + } + }, this.$slots.default) + } +} + +const AlertIcon = { + name: 'AlertIcon', + inject: ['_status', '_variant', '$colorMode', '$theme'], + props: { + size: { + default: 5 + }, + ...baseProps + }, + + render (h) { + const alertIconStyles = useAlertIconStyle({ + variant: this._variant, + colorMode: this.$colorMode, + color: statuses[this._status] && statuses[this._status]['color'] + }) + + return h(Icon, { + props: { + mr: this.$props.mr || 3, + size: this.size, + name: statuses[this._status] && statuses[this._status]['icon'], + ...alertIconStyles, + ...forwardProps(this.$props) + }, + attrs: { + focusable: false + } + }) + } +} + +const AlertTitle = { + name: 'AlertTitle', + props: { + ...baseProps + }, + render (h) { + return h(Box, { + props: { + fontWeight: 'bold', + lineHeight: 'normal', + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +const AlertDescription = { + name: 'AlertDescription', + props: { + ...baseProps + }, + render (h) { + return h(Box, { props: forwardProps(this.$props) }, this.$slots.default) + } +} + +export { Alert, AlertIcon, AlertTitle, AlertDescription } diff --git a/packages/kiwi-ui/src/Badge/badge.styles.js b/packages/kiwi-ui/src/Badge/badge.styles.js new file mode 100644 index 00000000..8653b5e6 --- /dev/null +++ b/packages/kiwi-ui/src/Badge/badge.styles.js @@ -0,0 +1,69 @@ +import { addOpacity, generateAlphas, get } from '../utils' + +const solidStyle = ({ theme: { colors }, color }) => { + const _color = colors[color] && colors[color][500] + const darkModeBg = addOpacity(_color, 0.6) + return { + light: { + bg: get(color, 500), + color: 'white' + }, + dark: { + bg: darkModeBg, + color: 'whiteAlpha.800' + } + } +} + +const subtleStyle = ({ theme: { colors }, color }) => { + const _color = colors[color] && colors[color][200] + const alphaColors = generateAlphas(_color) + const darkModeBg = alphaColors[300] + + return { + light: { + bg: get(color, 100), + color: get(color, 800) + }, + dark: { + bg: darkModeBg, + color: get(color, 200) + } + } +} + +const outlineStyle = ({ theme: { colors }, color }) => { + const _color = colors[color] && colors[color][200] + const darkModeColor = addOpacity(_color, 0.8) + const boxShadowColor = colors[color] && colors[color][500] + return { + light: { + boxShadow: `inset 0 0 0px 1px ` + boxShadowColor, + color: get(color, 500) + }, + dark: { + boxShadow: `inset 0 0 0px 1px ` + darkModeColor, + color: darkModeColor + } + } +} + +const variantProps = props => { + const { variant, colorMode } = props + switch (variant) { + case 'solid': + return solidStyle(props)[colorMode] + case 'subtle': + return subtleStyle(props)[colorMode] + case 'outline': + return outlineStyle(props)[colorMode] + default: + return {} + } +} + +const useBadgeStyle = props => { + return variantProps(props) +} + +export default useBadgeStyle diff --git a/packages/kiwi-ui/src/Badge/badge.test.js b/packages/kiwi-ui/src/Badge/badge.test.js new file mode 100644 index 00000000..2eae82c3 --- /dev/null +++ b/packages/kiwi-ui/src/Badge/badge.test.js @@ -0,0 +1,21 @@ +import { shallowMount } from '@vue/test-utils' +import Badge from './' +import theme from '../../lib/theme' + +describe('===== Badge Component =====', () => { + describe('Instance TEsts', () => { + it('should be a Vue instance', () => { + const badge = shallowMount(Badge, { + provide () { + return { + $theme: () => theme, + $colorMode: 'light' + } + } + }) + expect(badge.isVueInstance()).toBeTruthy() + expect(badge.vm.$theme()).toBe(theme) + expect(badge.vm.$colorMode).toBe('light') + }) + }) +}) diff --git a/packages/kiwi-ui/src/Badge/index.d.ts b/packages/kiwi-ui/src/Badge/index.d.ts new file mode 100644 index 00000000..7219bce4 --- /dev/null +++ b/packages/kiwi-ui/src/Badge/index.d.ts @@ -0,0 +1,19 @@ +import Vue from 'vue' + +export interface IBadge { + /** + * The color scheme of the badge + */ + variantColor?: string; + /** + * The variant of the badge + */ + variant?: "solid" | "subtle" | "outline"; +} + +/** + * The Badge component is used for state, general text, and number labels. + */ +declare const Badge: Vue.Component<{}>; + +export default Badge; diff --git a/packages/kiwi-ui/src/Badge/index.js b/packages/kiwi-ui/src/Badge/index.js new file mode 100644 index 00000000..784a5dc7 --- /dev/null +++ b/packages/kiwi-ui/src/Badge/index.js @@ -0,0 +1,46 @@ +import Box from '../Box' +import { forwardProps } from '../utils' +import { baseProps } from '../config/props' +import useBadgeStyles from './badge.styles' + +export default { + name: 'Badge', + inject: ['$theme', '$colorMode'], + props: { + variant: { + type: String, + default: 'subtle' + }, + variantColor: { + type: String, + default: 'gray' + }, + _ref: { + type: HTMLElement + }, + ...baseProps + }, + render (h) { + const badgeStyleProps = useBadgeStyles({ + theme: this.$theme(), + colorMode: this.$colorMode, + color: this.variantColor, + variant: this.variant + }) + + return h(Box, { + props: { + d: 'inline-block', + textTransform: 'uppercase', + fontSize: 'xs', + px: 1, + rounded: 'sm', + fontWeight: 'bold', + whiteSpace: 'nowrap', + verticalAlign: 'middle', + ...badgeStyleProps, + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} diff --git a/packages/kiwi-ui/src/Box/index.js b/packages/kiwi-ui/src/Box/index.js new file mode 100644 index 00000000..e0e4ef7d --- /dev/null +++ b/packages/kiwi-ui/src/Box/index.js @@ -0,0 +1,77 @@ +import styled from 'vue-styled-components' +import { background, border, color, borderRadius, flexbox, grid, layout, position, shadow, space, typography, compose } from 'styled-system' +import { baseProps, propsConfig } from '../config/props' +import { forwardProps } from '../utils/' + +const baseEllipsis = { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' +} + +/** + * @description Truncates text if `truncate` is set to true. + * @param {Object} props Props + */ +const truncate = props => { + if (props.truncate) { + if (!props.lineClamp) { + return baseEllipsis + } + } +} + +/** + * @description Clamps text based on number of lines. + * @param {Object} props Props + */ +const clamp = props => { + if (props.lineClamp) { + return { + ...baseEllipsis, + '-webkit-box-orient': 'vertical', + '-webkit-line-clamp': `${props.lineClamp}` + } + } +} + +const decorate = props => { + if (props.textDecoration || props.textDecor) { + return { + 'text-decoration': `${props.textDecoration || props.textDecor}` + } + } +} + +const system = compose( + layout, + color, + space, + background, + border, + borderRadius, + grid, + position, + shadow, + decorate, + typography, + flexbox, + propsConfig, + truncate, + clamp +) + +/** + * The Box component is the base reusable component which is the building block for all other Kiwi UI components. + * It by default renders the `
` element. + */ +const Box = styled('div', { + ...baseProps +})` + ${props => { + const sanitizedProps = forwardProps(props) + return system(sanitizedProps) + }} +` + +export default Box diff --git a/packages/kiwi-ui/src/Button/button.props.js b/packages/kiwi-ui/src/Button/button.props.js new file mode 100644 index 00000000..d84e25ea --- /dev/null +++ b/packages/kiwi-ui/src/Button/button.props.js @@ -0,0 +1,69 @@ +export const buttonProps = { + as: { + type: [String, Object], + default: 'button' + }, + type: { + type: String, + default: 'button' + }, + cast: { + type: String, + default: 'primary', + validator: (value) => + value.match(/^(primary|secondary|success|warning|danger)$/) + }, + variant: { + type: String, + default: 'solid', + validator: (value) => + value.match(/^(solid|outline|ghost|flat|link)$/) + }, + variantColor: { + type: [String, Array], + default: 'gray' + }, + disabled: { + type: Boolean, + default: false + }, + isLoading: { + type: Boolean, + default: false + }, + isActive: { + type: Boolean, + default: false + }, + size: { + type: String, + default: 'md', + validator: (value) => value.match(/^(sm|md|lg)$/) + }, + loadingText: { + type: String, + default: null + }, + iconSpacing: { + type: [String, Number], + default: 2, + validator: (value) => value >= 0 + }, + leftIcon: { + type: String, + default: null + }, + rightIcon: { + type: String, + default: null + }, + rounded: { + type: Boolean, + default: false + }, + ripple: { + type: [String, Boolean], + default: true + }, + _ref: Object +} diff --git a/packages/kiwi-ui/src/Button/button.styles.js b/packages/kiwi-ui/src/Button/button.styles.js new file mode 100644 index 00000000..0ce4dba2 --- /dev/null +++ b/packages/kiwi-ui/src/Button/button.styles.js @@ -0,0 +1,264 @@ +import { addOpacity } from '../utils' +const baseStyles = { + display: 'inline-flex', + appearance: 'none', + alignItems: 'center', + justifyContent: 'center', + transition: 'all 250ms', + userSelect: 'none', + position: 'relative', + whiteSpace: 'nowrap', + verticalAlign: 'middle', + lineHeight: '1.2', + outline: 'none' +} + +const buttonSizes = { + xs: { + w: '0.75rem', + h: '0.75rem' + }, + sm: { + w: '1rem', + h: '1rem' + }, + md: { + w: '1.5rem', + h: '1.5rem' + }, + lg: { + w: '2rem', + h: '2rem' + }, + xl: { + w: '3rem', + h: '3rem' + } +} + +const createCustomSize = (size) => { + return { + w: size, + h: size + } +} + +/** + * @description Evaluates button icon sizes and returns wight and height parameters + * @param {Object} props + */ +export const setIconSizes = (props) => { + return buttonSizes[props.size] || createCustomSize(props.size) +} + +/** + * Size values + */ +const sizes = { + lg: { + h: 12, + minW: 12, + fontSize: 'lg', + px: 6 + }, + md: { + h: 10, + minW: 10, + fontSize: 'md', + px: 4 + }, + sm: { + h: 8, + minW: 8, + fontSize: 'sm', + px: 3 + }, + xs: { + h: 6, + minW: 6, + fontSize: 'xs', + px: 2 + } +} + +/** + * @description Determines size props + * @param {Object} param0 Props object + * @returns {Object} Size style props + */ +const sizeProps = ({ size }) => sizes[size] + +/** + * @description Get solid button style values + * @param {Object} props - Style props object + * @returns {Object} - Solid styles object + */ +const getSolidStyles = ({ color, colorMode }) => { + let style = { + light: { + bg: `${color}.400`, + color: 'white', + _hover: { + bg: `${color}.500` + }, + _active: { + bg: `${color}.600` + } + } + } + return style[colorMode] +} + +/** + * @description Get ghost button style values + * @param {Object} props - Style Props + * @returns {Object} - Ghost styles object + */ +const getGhostStyles = ({ color, colorMode, theme }) => { + const _theme = theme() + const _color = _theme.colors[color] && _theme.colors[color][200] + let result = { + light: { + color: `${color}.400`, + bg: `${color}.50`, + _hover: { + bg: `${color}.100` + }, + _active: { + bg: `${color}.200` + } + }, + dark: { + color: `${color}.200`, + bg: 'transparent', + _hover: { + bg: addOpacity(_color, 0.12) + }, + _active: { + bg: addOpacity(_color, 0.24) + } + } + } + return result[colorMode] +} + +/** + * @description Get flat button style values + * @param {Object} props - Style Props + * @returns {Object} - Ghost styles object + */ +const getFlatStyles = ({ color, colorMode, theme }) => { + const _theme = theme() + const _color = _theme.colors[color] && _theme.colors[color][200] + let result = { + light: { + color: `${color}.400`, + bg: 'transparent', + _hover: { + bg: `${color}.50` + }, + _active: { + bg: `${color}.100` + } + }, + dark: { + color: `${color}.200`, + bg: 'transparent', + _hover: { + bg: addOpacity(_color, 0.12) + }, + _active: { + bg: addOpacity(_color, 0.24) + } + } + } + return result[colorMode] +} + +/** + * @description Get outline button style values + * @param {Object} props - Style props object + * @returns {Object} - Solid styles object + */ +const getOutlineStyles = props => { + const { color, colorMode } = props + const borderColor = { light: 'gray.200', dark: 'whiteAlpha.300' } + + return { + border: '1px', + borderColor: color === 'gray' ? borderColor[colorMode] : 'current', + ...getFlatStyles(props) + } +} + +/** + * @description Get link button style values + * @param {Object} props - Style props object + * @returns {Object} - Solid styles object + */ +const getLinkStyles = ({ color, colorMode }) => { + const _color = { light: `${color}.400`, dark: `${color}.200` } + const _activeColor = { light: `${color}.700`, dark: `${color}.500` } + return { + p: 0, + height: 'auto', + lineHeight: 'normal', + color: _color[colorMode], + _hover: { + // TODO: Figure out why styled system doesn't support `textDecoration` property. + textDecoration: 'underline' + }, + _active: { + color: _activeColor[colorMode] + } + } +} + +/** + * @description Determines styles for a given v + * @param {Object} props - Props Object + * @returns {Object} - Variant styles object + */ +const getVariantStyles = (props) => { + switch (props.variant) { + case 'solid': + return getSolidStyles(props) + case 'outline': + return getOutlineStyles(props) + case 'ghost': + return getGhostStyles(props) + case 'flat': + return getFlatStyles(props) + case 'link': + return getLinkStyles(props) + default: + return {} + } +} + +/** + * Button focus styles + */ +const focusStyles = { + _focus: { + outline: 'none', + boxShadow: 'outline' + } +} + +/** + * @description Generates Button styles based on passed variant props and theme colors. + * @param {{color: String|Array, theme: Object, colorMode: String, size: String|Array}} props - Style props object + * @returns {Object} Style object to be passed to styled component + * @todo Pass the `theme` from the ThemeProvider context. Will need to create a context provider for theme. + */ +const createButtonStyles = (props) => { + return { + ...baseStyles, + ...focusStyles, + ...sizeProps(props), + ...getVariantStyles(props) + } +} + +export default createButtonStyles diff --git a/packages/kiwi-ui/src/Button/button.test.js b/packages/kiwi-ui/src/Button/button.test.js new file mode 100644 index 00000000..dd77ce6f --- /dev/null +++ b/packages/kiwi-ui/src/Button/button.test.js @@ -0,0 +1,20 @@ +import { shallowMount } from '@vue/test-utils' +import Button from '../Button' +import Theme from '../../../kiwi.config' + +describe('===== Button Component =====', () => { + let button + describe('Instance Tests', () => { + it('should be a Vue instance', () => { + button = shallowMount(Button, { + provide () { + return { + $theme: () => Theme, + $colorMode: 'light' + } + } + }) + expect(button.isVueInstance()).toBeTruthy() + }) + }) +}) diff --git a/packages/kiwi-ui/src/Button/index.d.ts b/packages/kiwi-ui/src/Button/index.d.ts new file mode 100644 index 00000000..a4d090ac --- /dev/null +++ b/packages/kiwi-ui/src/Button/index.d.ts @@ -0,0 +1,20 @@ +import * as Vue from 'vue'; + +/** + * Kiwi Button component for UI interactions + */ +declare const Button: Vue.Component<{ + as?: String, + type?: String, + cast?: String, + variant?: String + variantColor?: [String, Array], + disabled?: Boolean, + isLoading?: Boolean, + isActive?: Boolean, + size?: String, + loadingText?: String, + iconSpacing?: String, + rounded?: Boolean, +}>; +export default Button; diff --git a/packages/kiwi-ui/src/Button/index.js b/packages/kiwi-ui/src/Button/index.js new file mode 100644 index 00000000..4b91faf8 --- /dev/null +++ b/packages/kiwi-ui/src/Button/index.js @@ -0,0 +1,124 @@ +import styleProps from '../config/props' +import { buttonProps } from './button.props' +import { forwardProps } from '../utils' +import createButtonStyles, { setIconSizes } from './button.styles' +import Box from '../Box' +import PseudoBox from '../PseudoBox' +import Spinner from '../Spinner' +import Icon from '../Icon' + +/** + * Icon component in button. + */ +const ButtonIcon = { + name: 'ButtonIcon', + props: { + icon: { + type: [String, Object] + }, + size: { + type: [String, Number] + }, + ...styleProps + }, + render (h) { + if (typeof this.icon === 'string') { + return h(Icon, { + props: { + focusable: false, + name: this.icon, + color: 'currentColor', + ...setIconSizes(this.$props), + ...forwardProps(this.$props) + } + }) + } else { + return h(Box, { + props: { + as: this.icon, + focusable: false, + color: 'currentColor', + ...setIconSizes(this.$props) + }, + attrs: { + 'data-custom-icon': true + } + }) + } + } +} + +/** + * @description The Button component is an accessible rich component that does what a button does :) + */ +export default { + name: 'Button', + inject: ['$theme', '$colorMode'], + props: { + ...buttonProps, + ...styleProps + }, + render (h) { + const buttonStyles = createButtonStyles({ + color: this.variantColor || this.cast, + variant: this.variant, + theme: this.$theme, + ripple: this.ripple, + colorMode: this.$colorMode, + size: this.size || 'md' + }) + + return h(PseudoBox, { + props: { + as: this.as, + outline: 'none', + cursor: 'pointer', + fontSize: 'md', + fontWeight: '700', + border: 'none', + transition: 'all 0.2s ease-in', + rounded: 'md', + width: this.isFullWidth ? 'full' : undefined, + ...buttonStyles, + ...forwardProps(this.$props) + }, + attrs: { + type: this.type, + tabIndex: 0, + disabled: this.disabled || this.isLoading, + ariaDisabled: this.disabled || this.isLoading, + dataActive: this.isActive ? 'true' : undefined + }, + on: { + click: ($event) => this.$emit('click', $event) + } + }, [ + this.leftIcon && !this.isLoading && h(ButtonIcon, { + props: { + mr: this.iconSpacing, + mb: 'px', + icon: this.leftIcon, + size: '1em' + } + }), + this.isLoading && h(Spinner, { + props: { + position: this.loadingText ? 'relative' : 'absolute', + color: 'currentColor', + mb: '-4px', + mr: this.loadingText ? this.iconSpacing : 0, + size: '1em' + } + }), + this.isLoading ? this.loadingText : this.$slots.default, + this.rightIcon && !this.isLoading && h(ButtonIcon, { + props: { + ml: this.iconSpacing, + mb: 'px', + icon: this.rightIcon, + size: '1em' + } + }) + ]) + } +} diff --git a/packages/kiwi-ui/src/Button/styles.js b/packages/kiwi-ui/src/Button/styles.js new file mode 100644 index 00000000..0f62ba62 --- /dev/null +++ b/packages/kiwi-ui/src/Button/styles.js @@ -0,0 +1,230 @@ +import { addOpacity } from '../../lib/utils' +const baseStyles = { + display: 'inline-flex', + appearance: 'none', + alignItems: 'center', + justifyContent: 'center', + transition: 'all 250ms', + userSelect: 'none', + position: 'relative', + whiteSpace: 'nowrap', + verticalAlign: 'middle', + lineHeight: '1.2', + outline: 'none' +} + +/** + * Size values + */ +const sizes = { + lg: { + height: 12, + minWidth: 12, + fontSize: 'lg', + px: 6, + py: 4 + }, + md: { + height: 10, + minWidth: 10, + fontSize: 'md', + px: 4, + py: 3 + }, + sm: { + height: 8, + minWidth: 8, + fontSize: 'sm', + px: 3, + py: 2 + }, + xs: { + height: 6, + minWidth: 6, + fontSize: 'xs', + px: 2, + py: 1 + } +} + +/** + * @description Determines size props + * @param {Object} param0 Props object + * @returns {Object} Size style props + */ +const sizeProps = ({ size }) => sizes[size] + +/** + * @description Get solid button style values + * @param {Object} props - Style props object + * @returns {Object} - Solid styles object + */ +const getSolidStyles = ({ color, colorMode }) => { + let style = { + light: { + bg: `${color}.400`, + color: 'white', + _hover: { + bg: `${color}.500` + }, + _active: { + bg: `${color}.600` + } + } + } + return style[colorMode] +} + +/** + * @description Get ghost button style values + * @param {Object} props - Style Props + * @returns {Object} - Ghost styles object + */ +const getGhostStyles = ({ color, colorMode, theme }) => { + const _theme = theme() + const _color = _theme.colors[color] && _theme.colors[color][200] + let result = { + light: { + color: `${color}.400`, + bg: `${color}.50`, + _hover: { + bg: `${color}.100` + }, + _active: { + bg: `${color}.200` + } + }, + dark: { + color: `${color}.200`, + bg: 'transparent', + _hover: { + bg: addOpacity(_color, 0.12) + }, + _active: { + bg: addOpacity(_color, 0.24) + } + } + } + return result[colorMode] +} + +/** + * @description Get flat button style values + * @param {Object} props - Style Props + * @returns {Object} - Ghost styles object + */ +const getFlatStyles = ({ color, colorMode, theme }) => { + const _theme = theme() + const _color = _theme.colors[color] && _theme.colors[color][200] + let result = { + light: { + color: `${color}.400`, + bg: 'transparent', + _hover: { + bg: `${color}.50` + }, + _active: { + bg: `${color}.100` + } + }, + dark: { + color: `${color}.200`, + bg: 'transparent', + _hover: { + bg: addOpacity(_color, 0.12) + }, + _active: { + bg: addOpacity(_color, 0.24) + } + } + } + return result[colorMode] +} + +/** + * @description Get outline button style values + * @param {Object} props - Style props object + * @returns {Object} - Solid styles object + */ +const getOutlineStyles = props => { + const { color, colorMode } = props + const borderColor = { light: 'gray.200', dark: 'whiteAlpha.300' } + + return { + border: '1px', + borderColor: color === 'gray' ? borderColor[colorMode] : 'current', + ...getFlatStyles(props) + } +} + +/** + * @description Get link button style values + * @param {Object} props - Style props object + * @returns {Object} - Solid styles object + */ +const getLinkStyles = ({ color, colorMode }) => { + const _color = { light: `${color}.400`, dark: `${color}.200` } + const _activeColor = { light: `${color}.700`, dark: `${color}.500` } + return { + p: 0, + height: 'auto', + lineHeight: 'normal', + color: _color[colorMode], + _hover: { + // TODO: Figure out why styled system doesn't support `textDecoration` property. + textDecoration: 'underline' + }, + _active: { + color: _activeColor[colorMode] + } + } +} + +/** + * @description Determines styles for a given v + * @param {Object} props - Props Object + * @returns {Object} - Variant styles object + */ +const getVariantStyles = (props) => { + switch (props.variant) { + case 'solid': + return getSolidStyles(props) + case 'outline': + return getOutlineStyles(props) + case 'ghost': + return getGhostStyles(props) + case 'flat': + return getFlatStyles(props) + case 'link': + return getLinkStyles(props) + default: + return {} + } +} + +/** + * Button focus styles + */ +const focusStyles = { + _focus: { + outline: 'none', + boxShadow: 'outline' + } +} + +/** + * @description Generates Button styles based on passed variant props and theme colors. + * @param {{color: String|Array, theme: Object, colorMode: String, size: String|Array}} props - Style props object + * @returns {Object} Style object to be passed to styled component + * @todo Pass the `theme` from the ThemeProvider context. Will need to create a context provider for theme. + */ +const createButtonStyles = (props) => { + return { + ...baseStyles, + ...focusStyles, + ...sizeProps(props), + ...getVariantStyles(props) + } +} + +export default createButtonStyles diff --git a/packages/kiwi-ui/src/CloseButton/index.d.ts b/packages/kiwi-ui/src/CloseButton/index.d.ts new file mode 100644 index 00000000..bb169275 --- /dev/null +++ b/packages/kiwi-ui/src/CloseButton/index.d.ts @@ -0,0 +1,29 @@ +import * as Vue from 'vue'; + +interface ICloseButton { + /** + * The size of the close button + */ + size?: "lg" | "md" | "sm"; + /** + * If `true`, the close button will be disabled + */ + isDisabled?: boolean; + /** + * The color of the close icon + */ + color?: string; + /** + * An accessible label for the close button + */ + "aria-label"?: string; +} + +export type CloseButtonProps = ICloseButton; + +/** + * Close button component for UI interfaces + */ +declare const CloseButton: Vue.Component; + +export default CloseButton; diff --git a/packages/kiwi-ui/src/CloseButton/index.js b/packages/kiwi-ui/src/CloseButton/index.js new file mode 100644 index 00000000..5f5509c0 --- /dev/null +++ b/packages/kiwi-ui/src/CloseButton/index.js @@ -0,0 +1,103 @@ +import Icon from '../Icon' +import PseudoBox from '../PseudoBox' +import styleProps from '../config/props' +import { forwardProps } from '../utils' + +const baseProps = { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + rounded: 'md', + transition: 'all 0.2s', + flex: '0 0 auto', + _hover: { bg: 'blackAlpha.100' }, + _active: { bg: 'blackAlpha.200' }, + _disabled: { + cursor: 'not-allowed' + }, + _focus: { + boxShadow: 'outline' + }, + border: 'none', + bg: 'blackAlpha.50' +} + +const sizes = { + lg: { + button: '40px', + icon: '16px' + }, + md: { + button: '32px', + icon: '12px' + }, + sm: { + button: '24px', + icon: '10px' + } +} + +export default { + name: 'CloseButton', + inject: ['$theme', '$colorMode'], + props: { + size: { + type: String, + default: 'md', + validator: value => value.match(/^(sm|md|lg)$/) + }, + isDisabled: { + type: Boolean, + default: false + }, + color: { + type: String, + default: 'currentColor' + }, + _ariaLabel: { + type: String, + default: 'Close' + }, + ...styleProps + }, + render (h) { + // Pseudo styles + const hoverColor = { light: 'blackAlpha.100', dark: 'whiteAlpha.100' } + const activeColor = { light: 'blackAlpha.200', dark: 'whiteAlpha.200' } + + // Size styles + const buttonSize = sizes[this.size] && sizes[this.size]['button'] + const iconSize = sizes[this.size] && sizes[this.size]['icon'] + + return h(PseudoBox, { + props: { + as: 'button', + outline: 'none', + h: buttonSize, + w: buttonSize, + disabled: this.isDisabled, + _hover: { bg: hoverColor[this.colorMode] }, + _active: { bg: activeColor[this.colorMode] }, + ...baseProps, + ...forwardProps(this.$props) + }, + on: { + click: ($e) => this.$emit('click', $e) + }, + attrs: { + ariaLabel: this._ariaLabel, + ariaDisabled: this.isDisabled + } + }, [h(Icon, { + props: { + color: this.color, + name: 'close', + size: iconSize + }, + attrs: { + focusable: false, + ariaHidden: true + } + })]) + } +} diff --git a/packages/kiwi-ui/src/Icon/icon.utils.js b/packages/kiwi-ui/src/Icon/icon.utils.js new file mode 100644 index 00000000..06fafb9d --- /dev/null +++ b/packages/kiwi-ui/src/Icon/icon.utils.js @@ -0,0 +1,26 @@ +// @ts-check + +/** + * @description Evaluate icon size props + * @param {Object} props - Props object + * @property {String} size - Props object + * @property {Object} theme - Props object + * @returns {Object} Size style props object + */ +const sizeProps = ({ size, theme }) => { + const _theme = theme() + return { + w: _theme.sizes[size], + h: _theme.sizes[size] + } +} + +/** + * @description Evaluate icon style props + * @param {Object} props - Props object + */ +export const iconStyles = (props) => { + return { + ...sizeProps(props) + } +} diff --git a/packages/kiwi-ui/src/Icon/index.d.ts b/packages/kiwi-ui/src/Icon/index.d.ts new file mode 100644 index 00000000..7515bbe8 --- /dev/null +++ b/packages/kiwi-ui/src/Icon/index.d.ts @@ -0,0 +1,13 @@ +import * as Vue from 'vue'; + +/** + * Icon component for UI representations + */ +declare const IconButton: Vue.Component<{ + name?: [String, Array], + use?: [String, Array], + pack?: String, + size?: [String, Number, Array], + color?: [String, Array], +}>; +export default IconButton; diff --git a/packages/kiwi-ui/src/Icon/index.js b/packages/kiwi-ui/src/Icon/index.js new file mode 100644 index 00000000..0e9bfc79 --- /dev/null +++ b/packages/kiwi-ui/src/Icon/index.js @@ -0,0 +1,81 @@ +import styled from 'vue-styled-components' +import Box from '../Box' +import iconPaths from '../lib/internal-icons' +import { forwardProps } from '../utils' +import { baseProps } from '../config/props' + +const fallbackIcon = iconPaths['question-outline'] + +const Svg = styled(Box)` + flex-shrink: 0; + backface-visibility: hidden; + &:not(:root) { + overflow: hidden; + } +` + +/** + * The Icon component renders SVGs for visual aid + */ +export default { + name: 'Icon', + inject: ['$theme', '$icons'], + props: { + name: { + type: [String, Array] + }, + use: { + type: [String, Array], + required: false + }, + pack: { + type: String, + required: false, + default: 'fas', + validator: value => value.match(/^(fas|fal|fad)$/) + }, + size: { + type: [String, Number, Array], + default: '1em' + }, + color: { + type: [String, Array], + default: 'currentColor' + }, + ...baseProps + }, + render (h) { + let icon, viewBox + if (this.name) { + icon = this.$icons[this.name] + } else { + console.warn(`[KiwiIcon]: You need to provide the "name" or "use" prop to for the Icon component`) + } + + if (!icon) { + icon = fallbackIcon + } + + viewBox = icon.viewBox || '0 0 24 24' + + return h(Svg, { + props: { + as: 'svg', + w: this.size, + h: this.size, + color: this.color, + d: 'inline-block', + verticalAlign: 'middle', + ...forwardProps(this.$props) + }, + attrs: { + viewBox, + role: 'presentation', + focusable: false + }, + domProps: { + innerHTML: icon.path + } + }) + } +} diff --git a/packages/kiwi-ui/src/IconButton/icon-button.utils.js b/packages/kiwi-ui/src/IconButton/icon-button.utils.js new file mode 100644 index 00000000..4e53f292 --- /dev/null +++ b/packages/kiwi-ui/src/IconButton/icon-button.utils.js @@ -0,0 +1,37 @@ +const iconSizes = { + xs: { + w: '0.75rem', + h: '0.75rem' + }, + sm: { + w: '0.8rem', + h: '0.8rem' + }, + md: { + w: '1.0rem', + h: '1.0rem' + }, + lg: { + w: '2rem', + h: '2rem' + }, + xl: { + w: '3rem', + h: '3rem' + } +} + +const createCustomSize = (size) => { + return { + w: size, + h: size + } +} + +/** + * @description Evaluates button icon sizes and returns wight and height parameters + * @param {Object} props + */ +export const setButtonIconSize = (props) => { + return iconSizes[props.size] || createCustomSize(props.size) +} diff --git a/packages/kiwi-ui/src/IconButton/index.d.ts b/packages/kiwi-ui/src/IconButton/index.d.ts new file mode 100644 index 00000000..febc40d6 --- /dev/null +++ b/packages/kiwi-ui/src/IconButton/index.d.ts @@ -0,0 +1,12 @@ +import * as Vue from 'vue'; + +/** + * Icon Button component for UI interactions + */ +declare const IconButton: Vue.Component<{ + icon?: String, + isRound?: Boolean, + _ariaLabel?: String, + _ref?: HTMLElement +}>; +export default IconButton; diff --git a/packages/kiwi-ui/src/IconButton/index.js b/packages/kiwi-ui/src/IconButton/index.js new file mode 100644 index 00000000..162bd91c --- /dev/null +++ b/packages/kiwi-ui/src/IconButton/index.js @@ -0,0 +1,77 @@ +import Button from '../Button' +import Icon from '../Icon' +import Box from '../Box' +import styleProps from '../config/props' +import { forwardProps } from '../utils' +import { buttonProps } from '../Button/button.props' + +const baseStyles = { + display: 'inline-flex', + appearance: 'none', + alignItems: 'center', + justifyContent: 'center', + transition: 'all 250ms', + userSelect: 'none', + position: 'relative', + whiteSpace: 'nowrap', + verticalAlign: 'middle', + lineHeight: '1.2', + outline: 'none' +} + +export default { + name: 'IconButton', + inject: ['$theme', '$colorMode'], + props: { + icon: { + type: [String] + }, + isRound: { + type: [Boolean] + }, + _ariaLabel: { + type: [String] + }, + ...buttonProps, + ...styleProps + }, + render (h) { + const { isFullWidth, leftIcon, rightIcon, loadingText, ...props } = this.$props + + return h(Button, { + props: { + p: 0, + rounded: this.isRound ? 'full' : 'md', + size: this.size, + ...forwardProps(props) + }, + attrs: { + 'aria-label': this._ariaLabel + } + }, + [typeof this.icon === 'string' + ? h(Icon, { + props: { + ...baseStyles, + name: this.icon, + color: 'currentColor', + mb: '2px', + size: '1em' + }, + attrs: { + focusable: false, + 'aria-hidden': true + } + }) + : h(Box, { + props: { + as: this.icon, + color: 'currentColor' + }, + attrs: { + focusable: true + } + })] + ) + } +} diff --git a/packages/kiwi-ui/src/PseudoBox/index.js b/packages/kiwi-ui/src/PseudoBox/index.js new file mode 100644 index 00000000..959c56b8 --- /dev/null +++ b/packages/kiwi-ui/src/PseudoBox/index.js @@ -0,0 +1,19 @@ +import styled from 'vue-styled-components' +import css from '@styled-system/css' +import Box from '../Box' +import { pseudoProps } from '../config/props' +import { parsePseudoStyles } from './utils' + +/** + * The PseudoBox component is a wrapper for the Box component that allows us to provide pseudo styles for `_focus`, `_hover`, `_active`, etc. and `aria-*` attributes + */ +const PseudoBox = styled(Box, { + ...pseudoProps +})` + ${(props) => { + const styles = parsePseudoStyles(props) + return css(styles) + } +}` + +export default PseudoBox diff --git a/packages/kiwi-ui/src/PseudoBox/utils.js b/packages/kiwi-ui/src/PseudoBox/utils.js new file mode 100644 index 00000000..51c9fcff --- /dev/null +++ b/packages/kiwi-ui/src/PseudoBox/utils.js @@ -0,0 +1,50 @@ +import map from 'lodash-es/map' +import { forwardProps, filterPseudo, tx } from '../utils' + +/** + * PseudoBox pseudo selectors + */ +export const selectors = { + _hover: '&:hover', + _active: '&:active, &[data-active=true]', + _focus: '&:focus', + _visited: '&:visited', + _even: '&:nth-of-type(even)', + _odd: '&:nth-of-type(odd)', + _disabled: '&:disabled, &:disabled:focus, &:disabled:hover, &[aria-disabled=true], &[aria-disabled=true]:focus, &[aria-disabled=true]:hover', + _checked: '&[aria-checked=true]', + _mixed: '&[aria-checked=mixed]', + _selected: '&[aria-selected=true]', + _invalid: '&[aria-invalid=true]', + _pressed: '&[aria-pressed=true]', + _readOnly: '&[aria-readonly=true], &[readonly]', + _first: '&:first-of-type', + _last: '&:last-of-type', + _expanded: '&[aria-expanded=true]', + _grabbed: '&[aria-grabbed=true]', + _notFirst: '&:not(:first-of-type)', + _notLast: '&:not(:last-of-type)', + _groupHover: '[role=group]:hover &', + _before: '&:before', + _after: '&:after', + _focusWithin: '&:focus-within', + _placeholder: '&::placeholder' +} + +/** + * Filter undefined props and parse pseudo style props to generate css styles object + * @param {Object} props - Props + * @returns {Object} Parsed pseudo styles object + */ +export function parsePseudoStyles (props) { + const styles = {} + const clean = forwardProps(props) + const pseudoProps = filterPseudo(clean) + const result = map(pseudoProps, (value, prop) => ({ prop, value })) + result.forEach((pair) => { + if (selectors[pair.prop]) { + styles[selectors[pair.prop]] = tx(pair.value) + } + }) + return styles +} diff --git a/packages/kiwi-ui/src/README.md b/packages/kiwi-ui/src/README.md new file mode 100644 index 00000000..e170a399 --- /dev/null +++ b/packages/kiwi-ui/src/README.md @@ -0,0 +1,39 @@ +
+

🥝 Kiwi UI

+

Build scalable, accessible, and light-weight, Vue.js applications with ease.

+ +### 📦 Components +A complete list of all components to be built can be found here 👇🏽. + +🥝 View Kiwi Components + +### Browsing Components +You can also view all developed components in Storybook! + +🔖 View Storybook + +## Development +This current verison of Kiwi uses a forked version of `vue-styled-components`. This will be replaced in the near future. I order to get started with the development environment, run the following commands to install all packages and then build `vue-styled-components` dist. And voila! You're good to go. You only need to run this once after running `yarn install` on this repository. + +```bash +yarn install +yarn build-vsc +yarn serve +``` + +#### Project TODO: +- [x] Setup Storybook for components UI +- [x] Theme Provider +- [x] Develop styling scheme for components with styled components +- [x] Setup Vue.js plugin system + - [x] Provide Theme + - [x] Observe theme and set it dynamically in javascript with ease. +- [x] Provide icons API for icons component +- [x] Accessibility (Focus) Styling +- [ ] Make Vue toaster API to render custom elements inside default slot +- [ ] Make `createContext` API for Kiwi + - [ ] This component should return provider and corresponding consumer. Should be used for component creation. +- [ ] Setup NPM distribution +- [ ] Set up type system for components with Typescript +- [ ] Publish `vue-styled-components` alternative and link as peer dependency + diff --git a/packages/kiwi-ui/src/Spinner/index.d.ts b/packages/kiwi-ui/src/Spinner/index.d.ts new file mode 100644 index 00000000..d93d19f7 --- /dev/null +++ b/packages/kiwi-ui/src/Spinner/index.d.ts @@ -0,0 +1,16 @@ +import * as Vue from 'vue'; + +/** + * The Spinner component is a loading component for users + */ +declare const Spinner: Vue.Component<{ + size?:String, + label?: String, + thickness?: String, + speed?: String, + color?: String, + emptyColor?: String, + _ref?: Object +}>; +export default Spinner; + diff --git a/packages/kiwi-ui/src/Spinner/index.js b/packages/kiwi-ui/src/Spinner/index.js new file mode 100644 index 00000000..0eb63235 --- /dev/null +++ b/packages/kiwi-ui/src/Spinner/index.js @@ -0,0 +1,97 @@ +import { keyframes } from 'vue-styled-components' +import { baseProps } from '../config/props' +import { forwardProps } from '../utils' +import Box from '../Box' +import VisuallyHidden from '../VisuallyHidden' + +const spin = keyframes` + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +` + +const sizes = { + xs: { + w: '0.75rem', + h: '0.75rem' + }, + sm: { + w: '1rem', + h: '1rem' + }, + md: { + w: '1.5rem', + h: '1.5rem' + }, + lg: { + w: '2rem', + h: '2rem' + }, + xl: { + w: '3rem', + h: '3rem' + } +} + +const createCustomSize = (size) => { + return { + w: size, + h: size + } +} + +const setSizes = (props) => { + return sizes[props.size] || createCustomSize(props.size) +} + +export default { + name: 'Spinner', + props: { + size: { + type: [String, Array], + default: 'md' + }, + label: { + type: String, + default: 'Loading...' + }, + thickness: { + type: [String, Array], + default: '2px' + }, + speed: { + type: [String, Array], + default: '0.45s' + }, + color: { + type: [String, Array], + default: 'gray.200' + }, + emptyColor: { + type: [String, Array], + default: 'transparent' + }, + _ref: Object, + ...baseProps + }, + render (h) { + return h(Box, { + ref: this._ref, + props: { + d: 'inline-block', + borderWidth: this.thickness, + borderBottomColor: this.emptyColor, + borderLeftColor: this.emptyColor, + borderStyle: 'solid', + rounded: 'full', + color: this.color, + animation: `${spin} ${this.speed} linear infinite`, + ...setSizes(this.$props), + ...forwardProps(this.$props) + } + }, this.label && h(VisuallyHidden, {}, this.label)) + } +} diff --git a/packages/kiwi-ui/src/Text/index.d.ts b/packages/kiwi-ui/src/Text/index.d.ts new file mode 100644 index 00000000..5661e0b7 --- /dev/null +++ b/packages/kiwi-ui/src/Text/index.d.ts @@ -0,0 +1,10 @@ +import * as Vue from 'vue'; + +/** + * The KText component is a composable text element. + */ +declare const KText: Vue.Component<{ + as?: String +}>; + +export default KText; diff --git a/packages/kiwi-ui/src/Text/index.js b/packages/kiwi-ui/src/Text/index.js new file mode 100644 index 00000000..dbc52a85 --- /dev/null +++ b/packages/kiwi-ui/src/Text/index.js @@ -0,0 +1,23 @@ +import Box from '../Box' +import { forwardProps } from '../utils' +import { baseProps } from '../config/props' + +export default { + name: 'KText', // <-- Vue does not allow components to be registered using built-in or reserved HTML elements as component id: like "Text". So need to rename this. + inject: ['$theme', '$colorMode'], + props: { + as: { + type: [String, Array], + default: 'p' + }, + ...baseProps + }, + render (h) { + return h(Box, { + props: { + as: this.as, + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} diff --git a/packages/kiwi-ui/src/ThemeProvider/index.d.ts b/packages/kiwi-ui/src/ThemeProvider/index.d.ts new file mode 100644 index 00000000..0ec52073 --- /dev/null +++ b/packages/kiwi-ui/src/ThemeProvider/index.d.ts @@ -0,0 +1,5 @@ +import * as Vue from 'vue'; + +declare const ThemeProvider: Vue.Component<{ theme?: Object }>; +export default ThemeProvider; + diff --git a/packages/kiwi-ui/src/ThemeProvider/index.js b/packages/kiwi-ui/src/ThemeProvider/index.js new file mode 100644 index 00000000..9b1275ba --- /dev/null +++ b/packages/kiwi-ui/src/ThemeProvider/index.js @@ -0,0 +1,29 @@ +const ThemeProvider = { + name: 'ThemeProvider', + props: { + theme: { + type: Object, + default: () => null + }, + colorMode: { + type: String, + default: 'light' + }, + icons: { + type: Object, + required: false + } + }, + provide () { + return { + $theme: () => this.theme, + $colorMode: this.colorMode, + $icons: this.icons + } + }, + render () { + return this.$slots.default + } +} + +export default ThemeProvider diff --git a/packages/kiwi-ui/src/ThemeProvider/index.test.js b/packages/kiwi-ui/src/ThemeProvider/index.test.js new file mode 100644 index 00000000..6616ce3b --- /dev/null +++ b/packages/kiwi-ui/src/ThemeProvider/index.test.js @@ -0,0 +1,34 @@ +import { shallowMount } from '@vue/test-utils' +import ThemeProvider from '../ThemeProvider' +import Theme from '../../../kiwi.config' + +describe('===== ThemeProvider Component =====', () => { + let themeProvider + const ChildComponent = { + inject: ['$theme', '$colorMode'], + render: h => h('div', {}) + } + + it('should be a Vue component', () => { + themeProvider = shallowMount(ThemeProvider, { + slots: { + default: [ChildComponent] + } + }) + expect(themeProvider.isVueInstance()).toBeTruthy() + }) + + it('should provide theme & default color mode to child components', () => { + themeProvider = shallowMount(ThemeProvider, { + slots: { + default: [ChildComponent] + }, + propsData: { + theme: Theme, + colorMode: 'light' + } + }) + expect(themeProvider.find(ChildComponent).vm.$theme()).toBe(Theme) + expect(themeProvider.find(ChildComponent).vm.$colorMode).toBe('light') + }) +}) diff --git a/packages/kiwi-ui/src/Toast/index.d.ts b/packages/kiwi-ui/src/Toast/index.d.ts new file mode 100644 index 00000000..eb46a6ed --- /dev/null +++ b/packages/kiwi-ui/src/Toast/index.d.ts @@ -0,0 +1,40 @@ +import { IAlert, AlertProps as BaseAlertProps } from "../Alert"; +import { BoxProps } from "../Box"; +import { Omit } from "../common-types"; +import { Position } from "toasted-notes"; +import * as React from "react"; + +export interface IToast extends IAlert { + /** + * The title of the toast. + */ + title?: string; + /** + * If `true` adds a close button to the toast. + */ + isClosable?: boolean; + /** + * Callback function to close the toast. + */ + onClose?: () => void; + /** + * The description of the toast + */ + description?: string; + /** + * Duration before dismiss in milliseconds, or `null` to never dismiss. + */ + duration?: number | null; + /** + * One of toasted-notes positions. + */ + position?: keyof typeof Position; +} + +interface RenderOption { + render?: (props: { onClose: () => void; id: string }) => React.ReactNode; +} +export type useToastOptions = IToast & RenderOption; +declare const useToast: () => (props: useToastOptions) => void; + +export default useToast; diff --git a/packages/kiwi-ui/src/Toast/index.js b/packages/kiwi-ui/src/Toast/index.js new file mode 100644 index 00000000..ffa47f94 --- /dev/null +++ b/packages/kiwi-ui/src/Toast/index.js @@ -0,0 +1,161 @@ +import Breadstick from 'breadstick' +import { Alert, AlertIcon, AlertTitle, AlertDescription } from '../Alert' +import Box from '../Box' +import CloseButton from '../CloseButton' +import ThemeProvider from '../ThemeProvider' +import { baseProps } from '../config/props' +import { forwardProps } from '../utils' + +// Create breadstick instance. +const breadstick = new Breadstick() + +/** + * Toast component + */ +const Toast = { + name: 'Toast', + props: { + status: { + type: String, + default: 'info' + }, + variant: { + type: String, + default: 'solid' + }, + id: { + type: String + }, + title: { + type: String, + default: '' + }, + isClosable: { + type: Boolean, + default: true + }, + onClose: { + type: Function, + default: () => null + }, + description: { + type: String, + default: '' + }, + ...baseProps + }, + render (h) { + return ( + + + + {this.title && {this.title}} + {this.description && {this.description}} + + {this.isClosable && ( + + )} + + ) + } +} + +/** + * @description Toast initialization API + * @param {Object} options + * @property {Object} theme + * @property {Object} icons + */ +function useToast ({ theme, icons }) { + /** + * @description Notify Method for Kiwi + * @param {Object} options + * @property {String} position + * @property {Number} duration + * @property {Function} render + * @property {String} title + * @property {String} description + * @property {String} status + * @property {String} variant + * @property {Boolean} isClosable + */ + function notify ({ + position = 'bottom', + duration = 5000, + render, + title, + description, + status, + variant = 'solid', + isClosable + }) { + const options = { + position, + duration + } + + if (render) { + return breadstick.notify( + ({ h, onClose, id }) => { + return h(ThemeProvider, { + props: { + theme + } + }, [render({ onClose, id })]) + }, + // ( + // {render({ onClose, id })} + // ), + options + ) + } + + /** + * @todo Need to battletest breadstick to RELIABLY support JSX API and render function API globally. + */ + breadstick.notify(({ h, onClose, id }) => { + return h(ThemeProvider, { + props: { + icons, + theme + } + }, [h(Toast, { + props: { + status, + variant, + id: `${id}`, + title, + isClosable, + onClose, + description + } + })]) + }, + options + ) + } + + return notify +} + +export default useToast diff --git a/packages/kiwi-ui/src/TransitionExpand/index.js b/packages/kiwi-ui/src/TransitionExpand/index.js new file mode 100644 index 00000000..f0e6a06c --- /dev/null +++ b/packages/kiwi-ui/src/TransitionExpand/index.js @@ -0,0 +1,91 @@ +/** + * @description The following script was adapted from Markus Oberlener's blog on transitioning height based on `v-show` and `v-if` directives. + * @see https://markus.oberlehner.net/blog/transition-to-height-auto-with-vue/ + */ +export default { + name: 'TransitionExpand', + functional: true, + mounted () { + const css = ` + * { + will-change: height; + transform: translateZ(0); + backface-visibility: hidden; + perspective: 1000px; + } + + .expand-enter-active, + .expand-leave-active { + transition: height 0.2s cubic-bezier(0.075, 0.82, 0.165, 1); + overflow: hidden; + } + .expand-enter, + .expand-leave-to { + height: 0; + } + ` + + const head = document.head || document.getElementsByTagName('head')[0] + const style = document.createElement('style') + + head.appendChild(style) + + style.type = 'text/css' + if (style.styleSheet) { + // This is required for IE8 and below. + style.styleSheet.cssText = css + } else { + style.appendChild(document.createTextNode(css)) + } + }, + render (h, context) { + const data = { + props: { + name: `expand` + }, + on: { + // After enter event + afterEnter (element) { + // eslint-disable-next-line no-param-reassign + element.style.height = `auto` + }, + enter (element) { + const { width } = getComputedStyle(element) + /* eslint-disable no-param-reassign */ + element.style.width = width + element.style.position = `absolute` + element.style.visibility = `hidden` + element.style.height = `auto` + /* eslint-enable */ + const { height } = getComputedStyle(element) + /* eslint-disable no-param-reassign */ + element.style.width = null + element.style.position = null + element.style.visibility = null + element.style.height = 0 + /* eslint-enable */ + + // eslint-disable-next-line no-unused-expressions + getComputedStyle(element).height + setTimeout(() => { + // eslint-disable-next-line no-param-reassign + element.style.height = height + }) + }, + leave (element) { + const { height } = getComputedStyle(element) + // eslint-disable-next-line no-param-reassign + element.style.height = height + + // eslint-disable-next-line no-unused-expressions + getComputedStyle(element).height + setTimeout(() => { + // eslint-disable-next-line no-param-reassign + element.style.height = 0 + }) + } + } + } + return h('transition', data, context.children) + } +} diff --git a/packages/kiwi-ui/src/VisuallyHidden/index.js b/packages/kiwi-ui/src/VisuallyHidden/index.js new file mode 100644 index 00000000..05260894 --- /dev/null +++ b/packages/kiwi-ui/src/VisuallyHidden/index.js @@ -0,0 +1,16 @@ +import styled from 'vue-styled-components' +import Box from '../Box' + +const VisuallyHidden = styled(Box)` + border: 0px; + clip: rect(0px, 0px, 0px, 0px); + height: 1px; + width: 1px; + margin: -1px; + padding: 0px; + overflow: hidden; + white-space: nowrap; + position: absolute; +` + +export default VisuallyHidden diff --git a/packages/kiwi-ui/src/babel.config.js b/packages/kiwi-ui/src/babel.config.js new file mode 100644 index 00000000..81a6a89f --- /dev/null +++ b/packages/kiwi-ui/src/babel.config.js @@ -0,0 +1,14 @@ +module.exports = { + presets: [ + '@babel/preset-env', + 'env' + ], + env: { + test: { + plugins: [ + 'transform-es2015-modules-commonjs', + 'transform-vue-jsx' + ] + } + } +} diff --git a/packages/kiwi-ui/src/config/props/index.js b/packages/kiwi-ui/src/config/props/index.js new file mode 100644 index 00000000..020ae732 --- /dev/null +++ b/packages/kiwi-ui/src/config/props/index.js @@ -0,0 +1,18 @@ +import baseProps from './props' +import pseudoProps from './pseudo' +export { default as propsConfig } from './props.config' + +export { + baseProps, + pseudoProps +} + +/** + * Style props object + */ +const styleProps = { + ...baseProps, + ...pseudoProps +} + +export default styleProps diff --git a/packages/kiwi-ui/src/config/props/props.config.js b/packages/kiwi-ui/src/config/props/props.config.js new file mode 100644 index 00000000..4db02526 --- /dev/null +++ b/packages/kiwi-ui/src/config/props/props.config.js @@ -0,0 +1,165 @@ +import { system } from 'styled-system' +/** + * This configuration is meant for the intent of: + * - creating new custom properties for certian styled props + * - creating shorthand properties for some long attribute names. + */ +export const config = { + roundedTop: { + properties: ['borderTopLeftRadius', 'borderTopRightRadius'], + scale: 'radii' + }, + roundedBottom: { + properties: ['borderBottomLeftRadius', 'borderBottomRightRadius'], + scale: 'radii' + }, + roundedLeft: { + properties: ['borderTopLeftRadius', 'borderBottomLeftRadius'], + scale: 'radii' + }, + roundedRight: { + properties: ['borderTopRightRadius', 'borderBottomRightRadius'], + scale: 'radii' + }, + roundedTopRight: { + property: 'borderTopRightRadius', + scale: 'radii' + }, + roundedTopLeft: { + property: 'borderTopLeftRadius', + scale: 'radii' + }, + roundedBottomRight: { + property: 'borderBottomRightRadius', + scale: 'radii' + }, + roundedBottomLeft: { + property: 'borderBottomLeftRadius', + scale: 'radii' + }, + rounded: { + property: 'borderRadius', + scale: 'radii' + }, + d: { + property: 'display' + }, + w: { + property: 'width', + scale: 'sizes' + }, + minW: { + property: 'minWidth', + scale: 'sizes' + }, + maxW: { + property: 'maxWidth', + scale: 'sizes' + }, + h: { + property: 'height', + scale: 'sizes' + }, + minH: { + property: 'minHeight', + scale: 'sizes' + }, + maxH: { + property: 'maxHeight', + scale: 'sizes' + }, + bgImg: { + property: 'backgroundImage' + }, + bgImage: { + property: 'backgroundImage' + }, + bgSize: { + property: 'backgroundSize' + }, + bgPos: { + property: 'backgroundPosition' + }, + bgRepeat: { + property: 'backgroundRepeat' + }, + pos: { + property: 'position' + }, + flexDir: { + property: 'flexDirection' + }, + shadow: { + property: 'boxShadow', + scale: 'shadows' + }, + b: { + property: 'border', + scale: 'borders' + }, + bl: { + property: 'border-left', + scale: 'borders' + }, + bt: { + property: 'border-top', + scale: 'borders' + }, + br: { + property: 'border-right', + scale: 'borders' + }, + bb: { + property: 'border-bottom', + scale: 'borders' + }, + textDecoration: { + property: 'textDecoration' + }, + overflowX: true, + overflowY: true, + textTransform: true, + animation: true, + appearance: true, + transform: true, + transformOrigin: true, + visibility: true, + whiteSpace: true, + userSelect: true, + pointerEvents: true, + wordBreak: true, + overflowWrap: true, + textOverflow: true, + boxSizing: true, + cursor: true, + resize: true, + transition: true, + listStyleType: true, + listStylePosition: true, + listStyleImage: true, + fill: { + property: 'fill', + scale: 'colors' + }, + stroke: { + property: 'stroke', + scale: 'colors' + }, + objectFit: true, + objectPosition: true, + backgroundAttachment: { + property: 'backgroundAttachment' + }, + outline: true, + float: true, + willChange: true +} + +config.bgAttachment = config.backgroundAttachment +config.textDecor = config.textDecoration +config.listStylePos = config.listStylePosition +config.listStyleImg = config.listStyleImage + +const styledConfig = system(config) + +export default styledConfig diff --git a/packages/kiwi-ui/src/config/props/props.js b/packages/kiwi-ui/src/config/props/props.js new file mode 100644 index 00000000..5c7aae27 --- /dev/null +++ b/packages/kiwi-ui/src/config/props/props.js @@ -0,0 +1,123 @@ +const baseProps = { + color: String, + bg: String, + w: [Number, String, Array], + width: [Number, String, Array], + minW: [Number, String, Array], + minWidth: [String, Array], + maxW: [Number, String, Array], + maxWidth: [String, Array], + h: [Number, String, Array], + height: [Number, String, Array], + minH: [String, Array], + minHeight: [Number, String, Array], + maxH: [String, Array], + maxHeight: [Number, String, Array], + d: [String, Array], + display: [String, Array], + lineClamp: [String, Number, Array], + truncate: [Boolean, Number], + pt: [Number, String, Array], + pr: [Number, String, Array], + pb: [Number, String, Array], + pl: [Number, String, Array], + px: [Number, String, Array], + py: [Number, String, Array], + p: [Number, String, Array], + mt: [Number, String, Array], + mr: [Number, String, Array], + ml: [Number, String, Array], + mb: [Number, String, Array], + mx: [Number, String, Array], + my: [Number, String, Array], + m: [Number, String, Array], + rounded: [String, Array], + roundedTop: [String, Array], + roundedBottom: [String, Array], + roundedLeft: [String, Array], + roundedRight: [String, Array], + roundedTopRight: [String, Array], + roundedTopLeft: [String, Array], + roundedBottomRight: [String, Array], + roundedBottomLeft: [String, Array], + b: [String, Array], + br: [String, Array], + bb: [String, Array], + bl: [String, Array], + bt: [String, Array], + border: [String, Array], + borderLeft: [String, Array], + borderRight: [String, Array], + borderTop: [String, Array], + borderBottom: [String, Array], + shadow: [Number, String, Array], + backgroundColor: String, + pos: [String, Array], + position: [String, Array], + flexDir: [String, Array], + bgImg: [String, Array], + bgImage: [String, Array], + bgSize: [String, Array], + bgPos: [String, Array], + bgRepeat: [String, Array], + borderWidth: [String, Array], + fontWeight: [String, Array], + fontFamily: [String, Array], + fontSize: [String, Array], + textAlign: [String, Array], + textDecoration: [String, Array], + overflow: [String, Array], + overflowX: [String, Array], + overflowY: [String, Array], + textTransform: [String, Array], + animation: [String, Array], + alignItems: [String, Array], + alignContent: [String, Array], + justifyContent: [String, Array], + flexWrap: [String, Array], + flexBasis: [String, Array], + flexDirection: [String, Array], + flex: [String, Array], + justifySelf: [String, Array], + alignSelf: [String, Array], + order: [String, Array], + outline: [String, Array], + cursor: [String, Array], + transition: [String, Array], + borderBottomColor: [String, Array], + borderTopColor: [String, Array], + borderLeftColor: [String, Array], + borderRightColor: [String, Array], + borderColor: [String, Array], + borderStyle: [String, Array], + whiteSpace: [String, Array], + verticalAlign: [String, Array], + lineHeight: [Number, String, Array], + appearance: [String, Array], + top: [String, Number, Array], + bottom: [String, Number, Array], + left: [String, Number, Array], + right: [String, Number, Array] + // transform: [String, Array], + // transformOrigin: [String, Array], + // visibility: [String, Array], + // userSelect: [String, Array], + // pointerEvents: [String, Array], + // wordBreak: [String, Array], + // overflowWrap: [String, Array], + // textOverflow: [String, Array], + // boxSizing: [String, Array], + // resize: [String, Array], + // listStyleType: [String, Array], + // listStylePosition: [String, Array], + // listStyleImage: [String, Array], + // fill: [String, Array], + // stroke: [String, Array], + // objectFit: [String, Array], + // objectPosition: [String, Array], + // backgroundAttachment: [String, Array], + // float: [String, Array], + // willChange: [String, Array] +} + +export default baseProps diff --git a/packages/kiwi-ui/src/config/props/pseudo.config.js b/packages/kiwi-ui/src/config/props/pseudo.config.js new file mode 100644 index 00000000..e69de29b diff --git a/packages/kiwi-ui/src/config/props/pseudo.js b/packages/kiwi-ui/src/config/props/pseudo.js new file mode 100644 index 00000000..9fc0da8b --- /dev/null +++ b/packages/kiwi-ui/src/config/props/pseudo.js @@ -0,0 +1,28 @@ +const pseudoProps = { + _hover: [Object, String, Array], + _active: [Object, String, Array], + _focus: [Object, String, Array], + _odd: [Object, String, Array], + _visited: [Object, String, Array], + _even: [Object, String, Array], + _disabled: [Object, String, Array], + _checked: [Object, String, Array], + _mixed: [Object, String, Array], + _selected: [Object, String, Array], + _invalid: [Object, String, Array], + _pressed: [Object, String, Array], + _readOnly: [Object, String, Array], + _first: [Object, String, Array], + _last: [Object, String, Array], + _expanded: [Object, String, Array], + _grabbed: [Object, String, Array], + _notFirst: [Object, String, Array], + _notLast: [Object, String, Array], + _groupHover: [Object, String, Array], + _before: [Object, String, Array], + _after: [Object, String, Array], + _focusWithin: [Object, String, Array], + _placeholder: [Object, String, Array] +} + +export default pseudoProps diff --git a/packages/kiwi-ui/src/index.js b/packages/kiwi-ui/src/index.js new file mode 100644 index 00000000..7e0e6ef3 --- /dev/null +++ b/packages/kiwi-ui/src/index.js @@ -0,0 +1,16 @@ +import Plugin from './plugin/index.js' +export default Plugin +export { default as ThemeProvider } from './ThemeProvider' +export { default as Box } from './Box' +export { default as PseudoBox } from './PseudoBox' +export { default as Button } from './Button' +export { default as IconButton } from './IconButton' +export { default as Icon } from './Icon' +export { default as Spinner } from './Spinner' +export { default as VisuallyHidden } from './VisuallyHidden' +export { default as TransitionExpand } from './TransitionExpand' +export { default as Text } from './Text' +export * from './Alert' +export { default as Badge } from './Badge' +export { default as CloseButton } from './CloseButton' +export { default as useToast } from './Toast' diff --git a/packages/kiwi-ui/src/lib/internal-icons.js b/packages/kiwi-ui/src/lib/internal-icons.js new file mode 100644 index 00000000..04a5e6e5 --- /dev/null +++ b/packages/kiwi-ui/src/lib/internal-icons.js @@ -0,0 +1,86 @@ +/** + * Internal icon paths + */ +const icons = { + star: { + path: ` + + ` + }, + email: { + path: ` + + + + + ` + }, + phone: { + viewBox: '0 0 14 14', + path: ` + + ` + }, + _info: { + path: ` + + ` + }, + '_warning-2': { + path: ` + ` + }, + '_check-circle': { + path: ` + ` + }, + _warning: { + path: ` + ` + }, + 'question-outline': { + viewBox: '0 0 24 24', + path: ` + + + + + + ` + }, + close: { + path: ` + ` + } +} + +export default icons diff --git a/packages/kiwi-ui/src/plugin/index.js b/packages/kiwi-ui/src/plugin/index.js new file mode 100644 index 00000000..213d227f --- /dev/null +++ b/packages/kiwi-ui/src/plugin/index.js @@ -0,0 +1,30 @@ +import { parsePackIcons } from '../utils/icons' +import internalIcons from '../lib/internal-icons' + +/** + * Kiwi Component library plugin + */ +const Kiwi = { + install (Vue, options = {}) { + let packIcons = {} + const extendedIcons = options.icons ? options.icons.extend || {} : {} + + if (options.icons && options.icons.iconPack) { + packIcons = parsePackIcons(options.icons.iconPack, options.icons.iconSet) + } + + const icons = { + ...internalIcons, + ...packIcons, + ...extendedIcons + } + + // Bind theme and icons to prototype + Vue.prototype.$kiwi = { + theme: options.theme, + icons + } + } +} + +export default Kiwi diff --git a/packages/kiwi-ui/src/utils/color.js b/packages/kiwi-ui/src/utils/color.js new file mode 100644 index 00000000..9960837d --- /dev/null +++ b/packages/kiwi-ui/src/utils/color.js @@ -0,0 +1,55 @@ +// import css from '@styled-system/css' +import Color from 'color' + +/** + * @description Gets color value from theme + * @param {String} color + * @param {Number} hue + * @returns {String} color + */ +export const get = (color, hue) => `${color}.${hue}` + +/** + * @description Add opacity to a color + * @param {String} color Hex color code + * @param {Object} opacity Opacity + * @returns String + */ +export const addOpacity = (color, opacity) => { + return Color(color) + .fade(1 - opacity) + .string() +} + +export const generateAlphas = color => ({ + 900: addOpacity(color, 0.92), + 800: addOpacity(color, 0.8), + 700: addOpacity(color, 0.6), + 600: addOpacity(color, 0.48), + 500: addOpacity(color, 0.38), + 400: addOpacity(color, 0.24), + 300: addOpacity(color, 0.16), + 200: addOpacity(color, 0.12), + 100: addOpacity(color, 0.08), + 50: addOpacity(color, 0.04) +}) + +/** + * @description Creates emphasis color values for color. + * @param {String} color + * @param {String} emphasis + * @returns {Object} Color alpha hue + */ +export const colorEmphasis = (color, emphasis) => { + switch (emphasis) { + case 'high': + return color + case 'medium': + return generateAlphas(color)[700] + case 'low': + return generateAlphas(color)[500] + case 'lowest': + return generateAlphas(color)[300] + default: + } +} diff --git a/packages/kiwi-ui/src/utils/icons.js b/packages/kiwi-ui/src/utils/icons.js new file mode 100644 index 00000000..f410d24c --- /dev/null +++ b/packages/kiwi-ui/src/utils/icons.js @@ -0,0 +1,35 @@ +import merge from 'lodash-es/merge' + +/** + * @description Parse all Font Awesome Icons + * @param {Object} iconSet - Registered Icons object + * @returns {Object} - All Font awesome icons parsed. + */ +const parseFAIcons = (iconSet) => { + const parseFAIcon = (iconObject) => { + const { icon } = iconObject + return { + [`${iconObject.iconName}`]: { + path: ``, + viewBox: `0 0 ${icon[0]} ${icon[1]}` + } + } + } + + const result = Object.values(iconSet) + .map(value => parseFAIcon(value)) + .reduce((target, source) => merge(target, source), {}) + + return result +} + +/** + * @description Parse Icon packs + * @param {String} pack Icon pack name + * @param {Object} iconSet Registered Icon set + * @returns {Object} Parsed pack icons object + */ +export const parsePackIcons = (pack, iconSet) => { + const packIcons = pack === 'fa' ? parseFAIcons(iconSet) : {} + return packIcons +} diff --git a/packages/kiwi-ui/src/utils/index.js b/packages/kiwi-ui/src/utils/index.js new file mode 100644 index 00000000..70c6f567 --- /dev/null +++ b/packages/kiwi-ui/src/utils/index.js @@ -0,0 +1,6 @@ +export { default as Logger } from './logger' +export { provideTheme } from './provide-theme' +export { transformAlias as tx } from './transform' +export { pickProperty as forwardProps, filterPseudo } from './object' +export { addOpacity, colorEmphasis, generateAlphas, get } from './color' +export { parsePackIcons } from './icons' diff --git a/packages/kiwi-ui/src/utils/logger.js b/packages/kiwi-ui/src/utils/logger.js new file mode 100644 index 00000000..96ef294a --- /dev/null +++ b/packages/kiwi-ui/src/utils/logger.js @@ -0,0 +1,61 @@ +export default class Logger { + constructor (options) { + this.isDebug = false + + /** + * @description Logs message to console. + * @param {*} payload - Payload + **/ + this.debug = (...payload) => { + const log = payload.length > 1 + ? payload + : payload[0] + console.log && console.log(log) + } + + /** + * @description Logs error message to console. + * @param {*} payload - Payload + **/ + this.error = (...payload) => { + const log = payload.length > 1 + ? payload + : payload[0] + console.error && console.error(log) + } + + /** + * @description Logs warning message to console. + * @param {*} payload - Payload + **/ + this.warn = (...payload) => { + const log = payload.length > 1 + ? payload + : payload[0] + console.warn && console.warn(log) + } + + /** + * @description Logs info message to console. + * @param {*} payload - Payload + **/ + this.info = (...payload) => { + const log = payload.length > 1 + ? payload + : payload[0] + console.info && console.info(log) + } + + /** + * @description Logs success message to console. + * @param {*} payload - Payload + **/ + this.success = (...payload) => { + const log = payload.length > 1 + ? payload + : payload[0] + console.info && console.info(log) + } + this.isDebug = options.debug + } +} diff --git a/packages/kiwi-ui/src/utils/object.js b/packages/kiwi-ui/src/utils/object.js new file mode 100644 index 00000000..df4d21b5 --- /dev/null +++ b/packages/kiwi-ui/src/utils/object.js @@ -0,0 +1,19 @@ +import pickBy from 'lodash-es/pickBy' +import startsWith from 'lodash-es/startsWith' + +/** + * @description Clears out all undefined properties from an object. + * @param {Object} props + * @returns {Object} Sanitized object with defined values. + */ +export function pickProperty (props) { + const pure = pickBy(props, (prop) => prop !== undefined) + return pure +} + +export function filterPseudo (props) { + const pseudos = pickBy(props, (_value, key) => { + return startsWith(key, '_') + }) + return pseudos +} diff --git a/packages/kiwi-ui/src/utils/provide-theme.js b/packages/kiwi-ui/src/utils/provide-theme.js new file mode 100644 index 00000000..5bc17092 --- /dev/null +++ b/packages/kiwi-ui/src/utils/provide-theme.js @@ -0,0 +1,10 @@ +import ThemeProvider from '../ThemeProvider' + +/** + * @description Provides Kiwi theme to component + * @param {Function} h Vue render function + * @param {Vue.Component} Component - Vue component + */ +export const provideTheme = (h, Component) => { + return h(ThemeProvider, {}, [h(Component)]) +} diff --git a/packages/kiwi-ui/src/utils/transform.js b/packages/kiwi-ui/src/utils/transform.js new file mode 100644 index 00000000..a0296f5e --- /dev/null +++ b/packages/kiwi-ui/src/utils/transform.js @@ -0,0 +1,46 @@ +import { propsConfig as config } from '../config/props' + +/** + * @description Transforms the custom prop alias to a format that styled-system CSS supports + * @param {*} prop - Prop + * @param {*} propValue - Prop value + * @returns {Object} Style object with transformed alias. + * @see chakra-ui PseudoBox tx_ method. + */ +function normalizeAlias (prop, propValue) { + const configKeys = Object.keys(config) + let result = {} + + if (configKeys.includes(prop)) { + const { properties, property } = config[prop] + if (properties) { + properties.forEach(_cssProp => (result[_cssProp] = propValue)) + } + if (property) { + result[property] = propValue + } + if (config[prop] === true) { + result[prop] = propValue + } + } else { + result[prop] = propValue + } + return result +} + +/** + * @description Transforms the alias prop object to style-system supported syntax + * @param {Object} props - Props object + * @returns {Object} Normalized Props object + */ +export const transformAlias = props => { + let result = {} + for (let prop in props) { + if (typeof props[prop] === 'object') { + result = { ...result, [prop]: transformAlias(props[prop]) } + } else { + result = { ...result, ...normalizeAlias(prop, props[prop]) } + } + } + return result +} diff --git a/src/App.vue b/src/App.vue index a7755d48..435434a4 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,13 +3,18 @@
+ + + Kiwi is the best Vue component library +