├── CNAME ├── .gitignore ├── favicon.png ├── assets ├── logo.png └── vendor.css ├── .babelrc ├── screenshots ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png └── screenshot4.png ├── src ├── NotificationStore.js ├── deferred.js ├── About.vue ├── ItemService.js ├── Notifications.vue ├── Notification.vue ├── main.js ├── ItemTile.vue ├── user │ └── User.vue ├── Auth.js ├── Modal.vue ├── home │ └── Home.vue ├── item │ └── Item.vue ├── edit │ └── Edit.vue └── App.vue ├── LICENSE ├── package.json ├── README.md ├── webpack.config.js └── index.html /CNAME: -------------------------------------------------------------------------------- 1 | yourdomain.com 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chinchang/vuejsfire-hackathon-starter/HEAD/favicon.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chinchang/vuejsfire-hackathon-starter/HEAD/assets/logo.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["transform-runtime"], 4 | "comments": false 5 | } 6 | -------------------------------------------------------------------------------- /screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chinchang/vuejsfire-hackathon-starter/HEAD/screenshots/screenshot1.png -------------------------------------------------------------------------------- /screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chinchang/vuejsfire-hackathon-starter/HEAD/screenshots/screenshot2.png -------------------------------------------------------------------------------- /screenshots/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chinchang/vuejsfire-hackathon-starter/HEAD/screenshots/screenshot3.png -------------------------------------------------------------------------------- /screenshots/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chinchang/vuejsfire-hackathon-starter/HEAD/screenshots/screenshot4.png -------------------------------------------------------------------------------- /src/NotificationStore.js: -------------------------------------------------------------------------------- 1 | export default { 2 | notifications: [], 3 | 4 | add (notification) { 5 | this.notifications.push(notification); 6 | }, 7 | 8 | remove (notification) { 9 | this.notifications.$remove(notification); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/deferred.js: -------------------------------------------------------------------------------- 1 | export default function deferred() { 2 | var d = {}; 3 | var promise = new Promise(function (resolve, reject) { 4 | d.resolve = resolve; 5 | d.reject = reject; 6 | }); 7 | 8 | d.promise = promise; 9 | return Object.assign(d, promise); 10 | } -------------------------------------------------------------------------------- /src/About.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ItemService.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // TODO: Add Item caching 3 | promises: {}, 4 | 5 | fetchItem (itemId) { 6 | var self = this, 7 | item; 8 | 9 | // If already being asked for, reuse promise. 10 | if (this.promises[itemId]) { 11 | return this.promises[itemId]; 12 | } 13 | 14 | var promise = firebase.database().ref('/items/' + itemId).once('value').then(function (result) { 15 | delete self.promises[itemId]; 16 | item = result.val(); 17 | item.uid = itemId; 18 | item.title = item.title || 'Untitled Item' 19 | // item.createdOnFormatted = self.getFormattedDate(item.createdOn); 20 | return item; 21 | }); 22 | 23 | this.promises[itemId] = promise; 24 | return promise; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Notifications.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 34 | 35 | 36 | 44 | -------------------------------------------------------------------------------- /src/Notification.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | 30 | 31 | 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Kushagra Gour 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuefire-hackathon-starter", 3 | "description": "A hackathon starter kit based on vue.js and firebase", 4 | "author": "Kushagra Gour ", 5 | "private": true, 6 | "scripts": { 7 | "dev": "webpack-dev-server --inline --hot", 8 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules; mv dist/index.html ./" 9 | }, 10 | "dependencies": { 11 | "babel-runtime": "^6.0.0", 12 | "vue": "^1.0.0", 13 | "vue-router": "^0.7.13" 14 | }, 15 | "devDependencies": { 16 | "babel-core": "^6.0.0", 17 | "babel-loader": "^6.0.0", 18 | "babel-plugin-transform-runtime": "^6.0.0", 19 | "babel-preset-es2015": "^6.0.0", 20 | "babel-preset-stage-2": "^6.0.0", 21 | "cross-env": "^1.0.6", 22 | "css-loader": "^0.23.0", 23 | "extract-text-webpack-plugin": "^1.0.1", 24 | "file-loader": "^0.8.4", 25 | "html-webpack-plugin": "^2.21.0", 26 | "json-loader": "^0.5.4", 27 | "url-loader": "^0.5.7", 28 | "vue-hot-reload-api": "^1.2.0", 29 | "vue-html-loader": "^1.0.0", 30 | "vue-loader": "^8.2.1", 31 | "vue-style-loader": "^1.0.0", 32 | "webpack": "^1.12.2", 33 | "webpack-dev-server": "^1.12.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import App from './App.vue'; 4 | import Home from './home/Home.vue'; 5 | import User from './user/User.vue'; 6 | import Item from './item/Item.vue'; 7 | import Edit from './edit/Edit.vue'; 8 | import About from './About.vue'; 9 | 10 | Vue.use(VueRouter); 11 | 12 | var router = new VueRouter(); 13 | 14 | router.beforeEach(function () { 15 | scrollToTop(); 16 | }); 17 | 18 | router.afterEach(function (transition) { 19 | if (transition.to.defaultTitle !== false) { 20 | document.title = 'VueFire Hackathon Starter'; 21 | } 22 | ga('set', 'page', transition.to.path); 23 | ga('send', 'pageview'); 24 | }); 25 | 26 | // Scrolls to the top with an easing 27 | function scrollToTop() { 28 | if (document.body.scrollTop === 0) return; 29 | document.body.scrollTop = document.body.scrollTop + (-document.body.scrollTop) * 0.14; 30 | requestAnimationFrame(scrollToTop); 31 | } 32 | 33 | router.map({ 34 | '/': { 35 | component: Home 36 | }, 37 | '/about': { 38 | component: About 39 | }, 40 | '/item/new/edit': { 41 | name: 'new', 42 | component: Edit, 43 | }, 44 | '/item/:itemId/edit': { 45 | name: 'edit', 46 | component: Edit, 47 | }, 48 | '/item/:itemId': { 49 | name: 'item', 50 | component: Item 51 | }, 52 | '/user/:userId': { 53 | name: 'user', 54 | component: User 55 | } 56 | }); 57 | 58 | router.start(App, 'app'); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VuejsFire Hackathon Starter 2 | 3 | VuejsvFire is a project starter kit based on [Vue.js]() JavaScript framework and [Firebase]() as backend. This starter kit is actually born out of my side-project [Tolks](). 4 | 5 | --- 6 | [Demo](http://kushagragour.in/vuejsfire-hackathon-starter/) • [Features](#features) • [Ingredients](#ingredients) • [Getting Started](#getting-started-with-development) • [Deployment](#deployment) • [Detailed Wiki](https://github.com/chinchang/vuejsfire-hackathon-starter/wiki/) 7 | --- 8 | 9 | ![](/screenshots/screenshot1.png) 10 | 11 | ### Features 12 | 13 | - Built over Vuejs' [webpack-simple template](https://github.com/vuejs-templates/webpack-simple) 14 | - ES6 ready 15 | - Single file components 16 | - In-built User Module 17 | - Twitter/Facebook Authentication 18 | - In-built notification system 19 | - Beautiful design and experience 20 | - Responsive UI 21 | 22 | ### Ingredients 23 | 24 | - [Vue.js webpack-simple template](https://github.com/vuejs-templates/webpack-simple) 25 | - [Firebase](https://firebase.google.com) 26 | - [Material Design Icons](https://materialdesignicons.com/) 27 | - Page loader by [Tobias Ahlin](http://tobiasahlin.com/spinkit/) 28 | - [Roboto](https://fonts.google.com/specimen/Roboto) font from Google fonts 29 | 30 | ### Getting started with development 31 | 32 | - Clone this repo. 33 | - Create a project in [Firebase](https://console.firebase.google.com/). Note, the app would also run with the default Firebase config included in the project. But its advised to replace it with your own Firebase project. 34 | - Turn on [Twitter](https://firebase.google.com/docs/auth/web/twitter-login) & [Facebook](https://firebase.google.com/docs/auth/web/facebook-login) in your Firebase project. 35 | - Replace the Firebase credentials in `auth.js`. 36 | - Run `npm install` to install all dependencies. 37 | - Run `npm run dev` to spin the local server and access your cool app on `localhost:8080`. 38 | 39 | ### Deployment 40 | 41 | - Run `npm run build` to build the project. 42 | - If you are using Github pages for deployment, you can simply run `build-gh-pages.sh` instead. 43 | 44 | [Read more about deployment](https://github.com/chinchang/vuejsfire-hackathon-starter/wiki#deployment). 45 | 46 | ### License 47 | 48 | Open source under The MIT License 49 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var ExtractTextPlugin = require("extract-text-webpack-plugin") 4 | var HtmlWebpackPlugin = require('html-webpack-plugin') 5 | 6 | module.exports = { 7 | entry: './src/main.js', 8 | output: { 9 | path: path.resolve(__dirname, './dist'), 10 | publicPath: 'dist/', 11 | filename: 'build.js' 12 | }, 13 | resolveLoader: { 14 | root: path.join(__dirname, 'node_modules'), 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.vue$/, 20 | loader: 'vue' 21 | }, 22 | { 23 | test: /\.js$/, 24 | loader: 'babel', 25 | exclude: /node_modules/ 26 | }, 27 | { 28 | test: /\.json$/, 29 | loader: 'json' 30 | }, 31 | { 32 | test: /\.html$/, 33 | loader: 'vue-html' 34 | }, 35 | { 36 | test: /\.(png|jpg|gif|svg)$/, 37 | loader: 'url', 38 | query: { 39 | limit: 10000, 40 | name: '[name].[ext]?[hash]' 41 | } 42 | } 43 | ] 44 | }, 45 | 46 | devServer: { 47 | historyApiFallback: true, 48 | noInfo: true 49 | }, 50 | devtool: '#eval-source-map' 51 | } 52 | 53 | 54 | if (process.env.NODE_ENV === 'production') { 55 | module.exports.devtool = '#source-map' 56 | 57 | module.exports.vue = { 58 | loaders: { 59 | css: ExtractTextPlugin.extract('css') 60 | } 61 | } 62 | 63 | // http://vuejs.github.io/vue-loader/workflow/production.html 64 | module.exports.plugins = (module.exports.plugins || []).concat([ 65 | new webpack.DefinePlugin({ 66 | 'process.env': { 67 | NODE_ENV: '"production"' 68 | } 69 | }), 70 | new ExtractTextPlugin('style.css'), 71 | new HtmlWebpackPlugin({ 72 | filename: 'index.html', 73 | template: 'index.html', 74 | inject: true, 75 | hash: true, 76 | minify: { 77 | removeComments: true, 78 | // collapseWhitespace: true, 79 | // removeAttributeQuotes: true 80 | // more options: 81 | // https://github.com/kangax/html-minifier#options-quick-reference 82 | }, 83 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 84 | chunksSortMode: 'dependency' 85 | }), 86 | new webpack.optimize.UglifyJsPlugin({ 87 | compress: { 88 | warnings: false 89 | } 90 | }), 91 | new webpack.optimize.OccurenceOrderPlugin() 92 | ]) 93 | } 94 | -------------------------------------------------------------------------------- /src/ItemTile.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 44 | 45 | 46 | 102 | -------------------------------------------------------------------------------- /src/user/User.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 85 | 86 | 92 | -------------------------------------------------------------------------------- /src/Auth.js: -------------------------------------------------------------------------------- 1 | import deferred from './deferred.js'; 2 | 3 | export class Auth { 4 | 5 | constructor() { 6 | var self = this; 7 | this.isLoggedIn = false; 8 | this.user = null; 9 | this.loginDeferred = deferred(); 10 | this.userDataDeferred = deferred(); 11 | 12 | // Initialize Firebase 13 | // Change parameters according to your firebase config 14 | var config = { 15 | apiKey: "AIzaSyC1DkebpFDcclAFDpt2zbu_mcFTi9tRnA8", 16 | authDomain: "cool-project-9a463.firebaseapp.com", 17 | databaseURL: "https://cool-project-9a463.firebaseio.com", 18 | storageBucket: "", 19 | }; 20 | firebase.initializeApp(config); 21 | 22 | firebase.auth().onAuthStateChanged(function(user) { 23 | if (user) { 24 | self.user = firebase.auth().currentUser; 25 | self.isLoggedIn = true; 26 | self.loginDeferred.resolve(); 27 | 28 | // Listen for future changes on user obj 29 | firebase.database().ref('users/' + self.user.uid).on('value', self.onUserDataChange.bind(self)); 30 | } else { 31 | self.user = null; 32 | self.isLoggedIn = false; 33 | } 34 | }); 35 | } 36 | 37 | onUserDataChange(snapshot) { 38 | Object.assign(this.user, snapshot.val()); 39 | this.userDataDeferred.resolve(this.user); 40 | } 41 | 42 | onLogin() { 43 | return this.loginDeferred.promise; 44 | } 45 | 46 | getUserData() { 47 | return this.userDataDeferred.promise; 48 | } 49 | 50 | login(provider) { 51 | var self = this; 52 | var provider; 53 | if (provider === 'fb') { 54 | provider = new firebase.auth.FacebookAuthProvider(); 55 | } else { 56 | provider = new firebase.auth.TwitterAuthProvider(); 57 | } 58 | 59 | return firebase.auth().signInWithPopup(provider).then(function(result) { 60 | // Save this user in the store 61 | firebase.database().ref('users/' + result.user.uid).update({ 62 | displayName: result.user.displayName, 63 | email: result.user.email, 64 | photoURL: result.user.providerData[0].photoURL, 65 | signedUpOn: Date.now() 66 | }); 67 | }).catch(function(error) { 68 | // Handle Errors here. 69 | var errorCode = error.code; 70 | var errorMessage = error.message; 71 | // The email of the user's account used. 72 | var email = error.email; 73 | // The firebase.auth.AuthCredential type that was used. 74 | var credential = error.credential; 75 | console.log(error) 76 | }); 77 | } 78 | 79 | logout() { 80 | firebase.auth().signOut().then(function () { 81 | location.reload(); 82 | }); 83 | } 84 | } 85 | 86 | // Export a singleton instance 87 | export let auth = new Auth(); -------------------------------------------------------------------------------- /src/Modal.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 57 | 58 | 59 | 107 | -------------------------------------------------------------------------------- /src/home/Home.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 50 | 51 | 81 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | VueFire Hackathon Starter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 63 | 64 | 65 | 66 | 67 | 68 | 75 | 76 | 89 | 90 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/item/Item.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 90 | 91 | 92 | 100 | -------------------------------------------------------------------------------- /src/edit/Edit.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 129 | 130 | 170 | -------------------------------------------------------------------------------- /assets/vendor.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /*! Hint.css (base version) - v2.3.1 - 2016-06-05 4 | * http://kushagragour.in/lab/hint/ 5 | * Copyright (c) 2016 Kushagra Gour; Licensed */ 6 | 7 | [class*=hint--]{position:relative;display:inline-block}[class*=hint--]:after,[class*=hint--]:before{position:absolute;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);transform:translate3d(0,0,0);visibility:hidden;opacity:0;z-index:1000000;pointer-events:none;-webkit-transition:.3s ease;-moz-transition:.3s ease;transition:.3s ease;-webkit-transition-delay:0s;-moz-transition-delay:0s;transition-delay:0s}[class*=hint--]:hover:after,[class*=hint--]:hover:before{visibility:visible;opacity:1;-webkit-transition-delay:.1s;-moz-transition-delay:.1s;transition-delay:.1s}[class*=hint--]:before{content:'';position:absolute;background:0 0;border:6px solid transparent;z-index:1000001}[class*=hint--]:after{background:#383838;color:#fff;padding:8px 10px;font-size:12px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;line-height:12px;white-space:nowrap}[class*=hint--][aria-label]:after{content:attr(aria-label)}[class*=hint--][data-hint]:after{content:attr(data-hint)}[aria-label='']:after,[aria-label='']:before,[data-hint='']:after,[data-hint='']:before{display:none!important}.hint--top-left:before,.hint--top-right:before,.hint--top:before{border-top-color:#383838}.hint--bottom-left:before,.hint--bottom-right:before,.hint--bottom:before{border-bottom-color:#383838}.hint--top:after,.hint--top:before{bottom:100%;left:50%}.hint--top:before{margin-bottom:-11px;left:calc(50% - 6px)}.hint--top:after{-webkit-transform:translateX(-50%);-moz-transform:translateX(-50%);transform:translateX(-50%)}.hint--top:hover:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--top:hover:after{-webkit-transform:translateX(-50%) translateY(-8px);-moz-transform:translateX(-50%) translateY(-8px);transform:translateX(-50%) translateY(-8px)}.hint--bottom:after,.hint--bottom:before{top:100%;left:50%}.hint--bottom:before{margin-top:-11px;left:calc(50% - 6px)}.hint--bottom:after{-webkit-transform:translateX(-50%);-moz-transform:translateX(-50%);transform:translateX(-50%)}.hint--bottom:hover:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--bottom:hover:after{-webkit-transform:translateX(-50%) translateY(8px);-moz-transform:translateX(-50%) translateY(8px);transform:translateX(-50%) translateY(8px)}.hint--right:before{border-right-color:#383838;margin-left:-11px;margin-bottom:-6px}.hint--right:after{margin-bottom:-14px}.hint--right:after,.hint--right:before{left:100%;bottom:50%}.hint--right:hover:after,.hint--right:hover:before{-webkit-transform:translateX(8px);-moz-transform:translateX(8px);transform:translateX(8px)}.hint--left:before{border-left-color:#383838;margin-right:-11px;margin-bottom:-6px}.hint--left:after{margin-bottom:-14px}.hint--left:after,.hint--left:before{right:100%;bottom:50%}.hint--left:hover:after,.hint--left:hover:before{-webkit-transform:translateX(-8px);-moz-transform:translateX(-8px);transform:translateX(-8px)}.hint--top-left:after,.hint--top-left:before{bottom:100%;left:50%}.hint--top-left:before{margin-bottom:-11px;left:calc(50% - 6px)}.hint--top-left:after{-webkit-transform:translateX(-100%);-moz-transform:translateX(-100%);transform:translateX(-100%);margin-left:12px}.hint--top-left:hover:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--top-left:hover:after{-webkit-transform:translateX(-100%) translateY(-8px);-moz-transform:translateX(-100%) translateY(-8px);transform:translateX(-100%) translateY(-8px)}.hint--top-right:after,.hint--top-right:before{bottom:100%;left:50%}.hint--top-right:before{margin-bottom:-11px;left:calc(50% - 6px)}.hint--top-right:after{-webkit-transform:translateX(0);-moz-transform:translateX(0);transform:translateX(0);margin-left:-12px}.hint--top-right:hover:after,.hint--top-right:hover:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--bottom-left:after,.hint--bottom-left:before{top:100%;left:50%}.hint--bottom-left:before{margin-top:-11px;left:calc(50% - 6px)}.hint--bottom-left:after{-webkit-transform:translateX(-100%);-moz-transform:translateX(-100%);transform:translateX(-100%);margin-left:12px}.hint--bottom-left:hover:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--bottom-left:hover:after{-webkit-transform:translateX(-100%) translateY(8px);-moz-transform:translateX(-100%) translateY(8px);transform:translateX(-100%) translateY(8px)}.hint--bottom-right:after,.hint--bottom-right:before{top:100%;left:50%}.hint--bottom-right:before{margin-top:-11px;left:calc(50% - 6px)}.hint--bottom-right:after{-webkit-transform:translateX(0);-moz-transform:translateX(0);transform:translateX(0);margin-left:-12px}.hint--bottom-right:hover:after,.hint--bottom-right:hover:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--large:after,.hint--medium:after,.hint--small:after{white-space:normal;line-height:1.4em}.hint--small:after{width:80px}.hint--medium:after{width:150px}.hint--large:after{width:300px}.hint--always:after,.hint--always:before{opacity:1;visibility:visible}.hint--always.hint--top:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--always.hint--top:after{-webkit-transform:translateX(-50%) translateY(-8px);-moz-transform:translateX(-50%) translateY(-8px);transform:translateX(-50%) translateY(-8px)}.hint--always.hint--top-left:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--always.hint--top-left:after{-webkit-transform:translateX(-100%) translateY(-8px);-moz-transform:translateX(-100%) translateY(-8px);transform:translateX(-100%) translateY(-8px)}.hint--always.hint--top-right:after,.hint--always.hint--top-right:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--always.hint--bottom:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--always.hint--bottom:after{-webkit-transform:translateX(-50%) translateY(8px);-moz-transform:translateX(-50%) translateY(8px);transform:translateX(-50%) translateY(8px)}.hint--always.hint--bottom-left:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--always.hint--bottom-left:after{-webkit-transform:translateX(-100%) translateY(8px);-moz-transform:translateX(-100%) translateY(8px);transform:translateX(-100%) translateY(8px)}.hint--always.hint--bottom-right:after,.hint--always.hint--bottom-right:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--always.hint--left:after,.hint--always.hint--left:before{-webkit-transform:translateX(-8px);-moz-transform:translateX(-8px);transform:translateX(-8px)}.hint--always.hint--right:after,.hint--always.hint--right:before{-webkit-transform:translateX(8px);-moz-transform:translateX(8px);transform:translateX(8px)} -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 74 | 75 | 123 | 124 | 422 | --------------------------------------------------------------------------------