├── src ├── css │ ├── _theme.css │ ├── card.css │ ├── index.css │ ├── _variables.css │ ├── tabs.css │ ├── navbar.css │ ├── button.css │ ├── radio.css │ ├── _ios.css │ └── _android.css ├── ui │ ├── GeoCard.vue │ ├── ProgressButton.vue │ ├── MenuButton.vue │ ├── RangeSlider.vue │ ├── Toggle.vue │ └── Avatar.vue ├── view │ ├── List.vue │ ├── Nav.vue │ ├── Tabs.vue │ ├── Scroll.vue │ ├── SideMenus.vue │ └── Overlay.vue └── index.js ├── .gitignore ├── app.js ├── variables.js ├── README.md ├── package.json └── webpack.config.js /src/css/_theme.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/css/card.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/GeoCard.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/List.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/Nav.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/Tabs.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | dist/ 4 | public/ 5 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var Vue = require('vue') 2 | Vue.use(require('./src/')) 3 | window.Vue = Vue 4 | -------------------------------------------------------------------------------- /src/css/index.css: -------------------------------------------------------------------------------- 1 | @import "_variables.css"; 2 | @import "button.css"; 3 | @import "radio.css"; 4 | @import "navbar.css"; 5 | @import "tabs.css"; 6 | 7 | @import "_android.css"; 8 | @import "_ios.css"; 9 | -------------------------------------------------------------------------------- /src/css/_variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vui-green: #42b983; 3 | --ios-green: #4cd964; 4 | --ios-blue: #027AFB; 5 | --android-green: #009A57; 6 | --menu-button-color: #666; 7 | --range-slider-color: #027AFB; 8 | } 9 | -------------------------------------------------------------------------------- /variables.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'vui-green': '#42b983', 3 | 'menu-button-color': '#666', 4 | 'range-slider-color': '#027AFB', 5 | 'ios-green': '#4cd964', 6 | 'ios-blue': '#027AFB', 7 | 'android-green': '#009A57', 8 | } 9 | -------------------------------------------------------------------------------- /src/css/tabs.css: -------------------------------------------------------------------------------- 1 | .v-tabs { 2 | display: flex; 3 | justify-content: center; 4 | color: #454545; 5 | background: #f7f7f7; 6 | border-top: 1px solid #ddd; 7 | height: 49px; 8 | line-height: 49px; 9 | } 10 | .v-tabs .v-tabs_item { 11 | flex: 1 1 0; 12 | overflow: hidden; 13 | white-space: nowrap; 14 | color: inherit; 15 | font-size: 14px; 16 | font-weight: 400; 17 | text-decoration: none; 18 | text-align: center; 19 | height: 100%; 20 | } 21 | -------------------------------------------------------------------------------- /src/css/navbar.css: -------------------------------------------------------------------------------- 1 | .v-navbar { 2 | display: flex; 3 | padding: 5px; 4 | border-top: 1px solid transparent; 5 | border-bottom: 1px solid #ddd; 6 | line-height: 43px; 7 | justify-content: space-between; 8 | align-content: center; 9 | } 10 | .v-navbar .v-navbar_title { 11 | flex: 1; 12 | text-align: center; 13 | align-items: center; 14 | font-size: 17px; 15 | font-weight: 500; 16 | } 17 | .v-navbar .v-navbar_left { 18 | align-items: flex-start; 19 | margin: auto; 20 | } 21 | .v-navbar .v-navbar_right { 22 | align-items: flex-end; 23 | margin: auto; 24 | } 25 | .v-navbar .v-button { 26 | padding: 4px 12px; 27 | font-weight: 400; 28 | -webkit-font-smoothing: initial; 29 | } 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | var Avatar = require('./ui/Avatar.vue') 3 | var Toggle = require('./ui/Toggle.vue') 4 | var MenuButton = require('./ui/MenuButton.vue') 5 | var ProgressButton = require('./ui/ProgressButton.vue') 6 | var RangeSlider = require('./ui/RangeSlider.vue') 7 | 8 | var Scroll = require('./view/Scroll.vue') 9 | var SideMenus = require('./view/SideMenus.vue') 10 | 11 | exports.install = function(Vue) { 12 | // register ui components 13 | Vue.component('vui-avatar', Avatar) 14 | Vue.component('vui-toggle', Toggle) 15 | Vue.component('vui-menu-button', MenuButton) 16 | Vue.component('vui-progress-button', ProgressButton) 17 | Vue.component('vui-range-slider', RangeSlider) 18 | 19 | // register views 20 | Vue.component('vui-scroll', Scroll) 21 | Vue.component('vui-side-menus', SideMenus) 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VUI 2 | 3 | A UI framework for mobile, built with Vue. 4 | 5 | [![Donate lepture](https://img.shields.io/badge/donate-lepture-green.svg)](https://lepture.herokuapp.com/?amount=1000&reason=lepture%2Fvui) 6 | 7 | 8 | ## Design Pattern 9 | 10 | VUI provides components that look like native UI in iOS, Android and 11 | Windows UI. However, VUI will not build those animations as in Material 12 | design. 13 | 14 | 15 | ## Development 16 | 17 | Clone site for preview: 18 | 19 | $ git clone git@github.com:vui/vui.github.io.git public 20 | 21 | Generate site in `public` directory with `jekyll`: 22 | 23 | $ cd public 24 | $ jekyll build -w 25 | 26 | Serve assets with webpack dev server: 27 | 28 | $ npm start 29 | 30 | Now visit `http://localhost:9090/ui/index.html`. **Don't forget /index.html**. 31 | -------------------------------------------------------------------------------- /src/css/button.css: -------------------------------------------------------------------------------- 1 | .v-button, button.v-button, a.v-button { 2 | display: inline-block; 3 | background-color: white; 4 | border: 1px solid #ddd; 5 | border-radius: 3px; 6 | color: #999; 7 | padding: 6px 12px; 8 | font: 600 normal 13px/24px sans-serif; 9 | cursor: pointer; 10 | text-decoration: none; 11 | outline: none; 12 | box-sizing: border-box; 13 | text-align: center; 14 | } 15 | .v-button { 16 | margin: 4px 2px; 17 | } 18 | .v-button:hover { 19 | opacity: 0.8; 20 | } 21 | .v-button:active { 22 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.08) inset, 0 0 4px rgba(0, 0, 0, 0.1) inset; 23 | } 24 | .v-button[disabled], 25 | .v-button[disabled]:hover { 26 | opacity: 0.68; 27 | cursor: not-allowed; 28 | } 29 | .v-button[disabled]:active { 30 | box-shadow: none; 31 | } 32 | .v-button.v-button--primary { 33 | background-color: var(--vui-green); 34 | color: white; 35 | border-color: transparent; 36 | } 37 | .v-button.v-button--block { 38 | display: block; 39 | width: 100%; 40 | margin: 10px 0; 41 | } 42 | -------------------------------------------------------------------------------- /src/css/radio.css: -------------------------------------------------------------------------------- 1 | label.v-radio, 2 | .v-radio label { 3 | -webkit-tap-highlight-color: transparent; 4 | } 5 | .v-radio { 6 | vertical-align: middle; 7 | } 8 | .v-radio input { 9 | -webkit-appearance: none; 10 | margin: 0 8px 0 2px; 11 | line-height: 1; 12 | outline: none; 13 | border: none; 14 | background: none; 15 | vertical-align: middle; 16 | } 17 | .v-radio input:before { 18 | position: relative; 19 | display: inline-block; 20 | content: ''; 21 | width: 1em; 22 | height: 0.4em; 23 | border: 2px solid #666; 24 | border-top: 0; 25 | border-right: 0; 26 | transform: rotate3d(0, 0, 1, -45deg) translate3d(4px, 0, 0); 27 | opacity: 0.18; 28 | } 29 | .v-radio input:checked:before { 30 | opacity: 1; 31 | } 32 | 33 | /* radio group */ 34 | ul.v-radio { 35 | list-style-type: none; 36 | padding: 0; 37 | margin: 0; 38 | } 39 | .v-radio li { 40 | border-bottom: 1px solid #ddd; 41 | margin-left: 2em; 42 | padding: 0.6em 0 0.4em; 43 | } 44 | .v-radio li:last-of-type { 45 | border-bottom: 0; 46 | } 47 | .v-radio li input { 48 | margin-left: -2.2em; 49 | } 50 | -------------------------------------------------------------------------------- /src/ui/ProgressButton.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 35 | 36 | 50 | -------------------------------------------------------------------------------- /src/view/Scroll.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 43 | 44 | 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vui", 3 | "version": "0.1.0-alpha1", 4 | "description": "UI framework for mobile built with vue.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --inline --hot", 8 | "build": "NODE_ENV=production webpack --progress --hide-modules", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/lepture/vui.git" 14 | }, 15 | "keywords": [ 16 | "UI", 17 | "Mobile", 18 | "Vue" 19 | ], 20 | "author": "Hsiaoming Yang ", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "autoprefixer": "^6.3.1", 24 | "babel-core": "^6.9.1", 25 | "babel-loader": "^6.1.0", 26 | "babel-plugin-transform-runtime": "^6.9.0", 27 | "babel-preset-es2015": "^6.9.0", 28 | "babel-runtime": "^6.9.2", 29 | "css-loader": "^0.23.0", 30 | "extract-text-webpack-plugin": "^1.0.1", 31 | "postcss": "^5.0.14", 32 | "postcss-css-variables": "^0.5.1", 33 | "postcss-import": "^8.1.2", 34 | "postcss-loader": "^0.9.1", 35 | "vue": "^1.0.24", 36 | "vue-hot-reload-api": "^1.3.2", 37 | "vue-html-loader": "^1.1.0", 38 | "vue-loader": "^8.5.2", 39 | "vue-style-loader": "^1.0.0", 40 | "webpack": "^1.13.1", 41 | "webpack-dev-server": "^1.14.1" 42 | }, 43 | "peerDependencies": { 44 | "vue": "^1.0.7" 45 | }, 46 | "dependencies": { 47 | "iscroll": "^5.1.3", 48 | "word-color": "^1.2.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ui/MenuButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 72 | -------------------------------------------------------------------------------- /src/view/SideMenus.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | 24 | 71 | -------------------------------------------------------------------------------- /src/ui/RangeSlider.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 30 | 31 | 79 | -------------------------------------------------------------------------------- /src/ui/Toggle.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | 22 | 85 | -------------------------------------------------------------------------------- /src/css/_ios.css: -------------------------------------------------------------------------------- 1 | /** Toggle */ 2 | .ios .v-toggle { 3 | width: 51px; 4 | } 5 | .ios .v-toggle--checked.v-toggle { 6 | background: var(--ios-green); 7 | border-color: var(--ios-green); 8 | } 9 | .ios .v-toggle .v-toggle_handle { 10 | box-shadow: 0 2px 7px rgba(0, 0, 0, 0.35), 0 1px 1px rgba(0, 0, 0, 0.15); 11 | } 12 | .ios .v-toggle--checked .v-toggle_handle { 13 | transform: translate3d(20px, 0, 0); 14 | } 15 | .ios .v-toggle:before { 16 | content: ''; 17 | position: absolute; 18 | display: block; 19 | left: 0; 20 | top: 0; 21 | width: 100%; 22 | height: 100%; 23 | border-radius: 20px; 24 | background: #e6e6e6; 25 | transition: opacity 0.3s; 26 | } 27 | .ios .v-toggle--checked:before { 28 | opacity: 0; 29 | } 30 | .ios .v-toggle:after { 31 | content: ''; 32 | position: absolute; 33 | display: block; 34 | left: 0; 35 | top: 0; 36 | width: 100%; 37 | height: 100%; 38 | background: white; 39 | border-radius: 20px; 40 | transform: scale3d(1, 1, 1); 41 | transition: transform 0.3s; 42 | } 43 | .ios .v-toggle--checked:after { 44 | transform: scale3d(0, 0, 0); 45 | opacity: 1; 46 | } 47 | 48 | 49 | /** Range Slider */ 50 | .ios .v-range-slider input::-webkit-slider-runnable-track { 51 | height: 1px; 52 | } 53 | .ios .v-range-slider input::-webkit-slider-thumb:before { 54 | background: var(--ios-blue); 55 | height: 1px; 56 | } 57 | .ios .v-range-slider input::-webkit-slider-thumb { 58 | box-shadow: 0 0 1px rgba(0,0,0,.3), 0 3px 5px rgba(0,0,0,0.2); 59 | } 60 | 61 | /** Radio Group */ 62 | .ios .v-radio input:before { 63 | opacity: 0; 64 | } 65 | .ios .v-radio li { 66 | border-bottom: 1px solid #ddd; 67 | margin-left: 2em; 68 | padding: 0.6em 0 0.4em; 69 | } 70 | .ios .v-radio li:last-of-type { 71 | border-bottom: 0; 72 | } 73 | .ios .v-radio li input { 74 | margin-left: -2.2em; 75 | } 76 | .ios .v-radio input:checked:before { 77 | opacity: 1; 78 | border-color: var(--ios-blue); 79 | } 80 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 3 | 4 | var cssLoader = 'vue-style!css?sourceMap!postcss' 5 | if (process.env.NODE_ENV === 'production') { 6 | cssLoader = ExtractTextPlugin.extract('vue-style', 'css?-reduceTransforms!postcss') 7 | } 8 | 9 | var contentBase = __dirname + '/public' 10 | var pkg = require('./package.json') 11 | 12 | var plugins = [new webpack.DefinePlugin({ 13 | 'process.env': { 14 | NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development') 15 | } 16 | })] 17 | 18 | 19 | var options = { 20 | entry: { 21 | app: [ 22 | './app.js', 23 | './src/css/index.css' 24 | ], 25 | }, 26 | 27 | output: { 28 | path: contentBase + '/assets', 29 | filename: 'app.js', 30 | }, 31 | 32 | module: { 33 | loaders: [ 34 | {test: /\.vue$/, loader: 'vue'}, 35 | {test: /\.css$/, loader: cssLoader}, 36 | ] 37 | }, 38 | 39 | vue: { 40 | loaders: { 41 | css: cssLoader, 42 | } 43 | }, 44 | 45 | postcss: function (pack) { 46 | // use webpack context 47 | return [ 48 | require('postcss-import')({path: './src/css', addDependencyTo: pack}), 49 | require('postcss-css-variables'), 50 | require('autoprefixer'), 51 | ] 52 | }, 53 | 54 | devServer: { 55 | contentBase: contentBase + '/_site', 56 | publicPath: '/assets/', 57 | host: '0.0.0.0', 58 | }, 59 | 60 | plugins: plugins, 61 | 62 | devtool: 'source-map', 63 | } 64 | 65 | if (process.env.NODE_ENV === 'production') { 66 | options.entry.vendor = Object.keys(pkg.dependencies) 67 | plugins.push(new ExtractTextPlugin('app.css', {disable: false})) 68 | plugins.push(new webpack.optimize.CommonsChunkPlugin("vendor", "vendor.js")) 69 | plugins.push(new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}})) 70 | plugins.push(new webpack.optimize.OccurenceOrderPlugin()) 71 | options.plugins = plugins 72 | } 73 | 74 | module.exports = options 75 | -------------------------------------------------------------------------------- /src/ui/Avatar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 83 | 84 | 113 | -------------------------------------------------------------------------------- /src/view/Overlay.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 54 | 55 | 116 | -------------------------------------------------------------------------------- /src/css/_android.css: -------------------------------------------------------------------------------- 1 | /** Toggle */ 2 | .android .v-toggle, 3 | .android .v-toggle.v-toggle--checked { 4 | border: 0; 5 | background: transparent; 6 | width: 44px; 7 | } 8 | .android .v-toggle::before { 9 | content: ''; 10 | position: absolute; 11 | left: 0; 12 | top: 4px; 13 | width: 100%; 14 | height: 14px; 15 | border-radius: 10px; 16 | background: rgba(0, 0, 0, 0.2); 17 | opacity: 1; 18 | transition: background 0.15s; 19 | } 20 | .android .v-toggle--checked::before { 21 | background: rgba(0, 154, 87, 0.5); 22 | } 23 | .android .v-toggle::after { 24 | display: none; 25 | } 26 | .android .v-toggle .v-toggle_handle { 27 | width: 22px; 28 | height: 22px; 29 | background: #f0f0f0; 30 | box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.4); 31 | transition-duration: .15s; 32 | } 33 | .android .v-toggle--checked .v-toggle_handle { 34 | transform: translate3d(22px, 0, 0); 35 | background: var(--android-green); 36 | } 37 | .android .v-toggle .v-toggle_handle::before { 38 | display: block; 39 | width: 22px; 40 | height: 22px; 41 | border-radius: 50%; 42 | content: ''; 43 | background: rgba(0, 0, 0, 0); 44 | transform: scale3d(1, 1, 1); 45 | transition: transform .15s; 46 | } 47 | .android .v-toggle:active .v-toggle_handle::before { 48 | background: rgba(0, 0, 0, 0.2); 49 | transform: scale3d(2.8, 2.8, 2.8); 50 | } 51 | .android .v-toggle--checked:active .v-toggle_handle::before { 52 | background: rgba(0, 154, 87, 0.2); 53 | } 54 | 55 | /** Range Slider */ 56 | .android .v-range-slider input::-webkit-slider-thumb:before { 57 | background: var(--android-green); 58 | top: 9px; 59 | } 60 | .android .v-range-slider input::-webkit-slider-thumb { 61 | background: var(--android-green); 62 | border: 0; 63 | box-shadow: none; 64 | width: 22px; 65 | height: 22px; 66 | margin-top: -9px; 67 | } 68 | 69 | /** Radio */ 70 | .android .v-radio input { 71 | position: relative; 72 | } 73 | .android .v-radio input:before { 74 | position: static; 75 | content: ''; 76 | width: 10px; 77 | height: 10px; 78 | border-radius: 50%; 79 | background: var(--android-green); 80 | opacity: 1; 81 | transform: scale3d(0, 0, 0); 82 | box-shadow: none; 83 | border: 0; 84 | transition: transform 0.15s; 85 | } 86 | .android .v-radio input:after { 87 | position: absolute; 88 | top: -4px; 89 | left: -4px; 90 | display: block; 91 | content: ''; 92 | width: 18px; 93 | height: 18px; 94 | border: 2px solid #5a5a5a; 95 | border-radius: 50%; 96 | box-sizing: border-box; 97 | transition: background 0.1s; 98 | } 99 | .android .v-radio input:checked:before { 100 | transform: scale3d(1, 1, 1); 101 | } 102 | .android .v-radio input:checked:after { 103 | border-color: var(--android-green); 104 | } 105 | .android .v-radio li { 106 | border: none; 107 | } 108 | --------------------------------------------------------------------------------