├── app ├── .gitkeep ├── components │ └── stagger-set.js └── styles │ └── ember-stagger-swagger.css ├── addon ├── .gitkeep ├── templates │ └── components │ │ └── stagger-set.hbs ├── components │ └── stagger-set.js ├── utils │ └── get-animation-prefix.js ├── constants │ └── constants.js └── mixins │ └── stagger-set.js ├── vendor └── .gitkeep ├── tests ├── unit │ ├── .gitkeep │ └── mixins │ │ └── stagger-set-test.js ├── integration │ ├── .gitkeep │ └── components │ │ └── stagger-set-test.js ├── dummy │ ├── app │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── .gitkeep │ │ ├── routes │ │ │ ├── .gitkeep │ │ │ ├── index.js │ │ │ └── homepage.js │ │ ├── styles │ │ │ └── app.css │ │ ├── components │ │ │ ├── .gitkeep │ │ │ ├── space-craft-card.js │ │ │ └── homepage-demo.js │ │ ├── templates │ │ │ ├── components │ │ │ │ ├── .gitkeep │ │ │ │ ├── space-craft-card.hbs │ │ │ │ └── homepage-demo.hbs │ │ │ ├── application.hbs │ │ │ └── homepage.hbs │ │ ├── resolver.js │ │ ├── router.js │ │ ├── app.js │ │ └── index.html │ ├── public │ │ ├── robots.txt │ │ ├── crossdomain.xml │ │ └── savvy.min.css │ └── config │ │ └── environment.js ├── test-helper.js ├── helpers │ ├── destroy-app.js │ ├── resolver.js │ ├── integration │ │ └── get-node.js │ ├── start-app.js │ ├── unit │ │ └── test-defaults-after-invalid-instantiation.js │ └── module-for-acceptance.js ├── .jshintrc └── index.html ├── .watchmanconfig ├── .bowerrc ├── blueprints ├── .jshintrc └── ember-stagger-swagger │ └── index.js ├── config ├── environment.js ├── release.js ├── ember-try.js └── constants │ └── constants.js ├── bower.json ├── .npmignore ├── testem.js ├── .ember-cli ├── .gitignore ├── .jshintrc ├── .editorconfig ├── ember-cli-build.js ├── LICENSE.md ├── .travis.yml ├── index.js ├── package.json ├── CHANGELOG.md └── README.md /app/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addon/templates/components/stagger-set.hbs: -------------------------------------------------------------------------------- 1 | {{yield}} 2 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /blueprints/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "console" 4 | ], 5 | "strict": false 6 | } 7 | -------------------------------------------------------------------------------- /app/components/stagger-set.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-stagger-swagger/components/stagger-set'; 2 | -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 'use strict'; 3 | 4 | module.exports = function(/* environment, appConfig */) { 5 | return { }; 6 | }; 7 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { 3 | setResolver 4 | } from 'ember-qunit'; 5 | 6 | setResolver(resolver); 7 | -------------------------------------------------------------------------------- /tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default function destroyApp(application) { 4 | Ember.run(application, 'destroy'); 5 | } 6 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/homepage.hbs: -------------------------------------------------------------------------------- 1 | {{homepage-demo 2 | staggerDirectionOptions=model.staggerDirectionOptions 3 | staggerListItems=model.staggerListItems 4 | }} 5 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/index.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { Route } = Ember; 4 | 5 | export default Route.extend({ 6 | 7 | beforeModel() { 8 | this.transitionTo('homepage'); 9 | }, 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-stagger-swagger", 3 | "dependencies": { 4 | "ember": "~2.5.0", 5 | "ember-cli-shims": "0.1.1", 6 | "ember-cli-test-loader": "0.2.2", 7 | "ember-qunit-notifications": "0.1.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /config/ember-try.js 3 | /dist 4 | /tests 5 | /tmp 6 | **/.gitkeep 7 | .bowerrc 8 | .editorconfig 9 | .ember-cli 10 | .gitignore 11 | .jshintrc 12 | .watchmanconfig 13 | .travis.yml 14 | bower.json 15 | ember-cli-build.js 16 | testem.js 17 | -------------------------------------------------------------------------------- /addon/components/stagger-set.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import layout from '../templates/components/stagger-set'; 3 | import StaggerSetMixin from '../mixins/stagger-set'; 4 | 5 | const { Component } = Ember; 6 | 7 | export default Component.extend(StaggerSetMixin, { 8 | layout 9 | }); 10 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | "framework": "qunit", 4 | "test_page": "tests/index.html?hidepassed", 5 | "disable_watching": true, 6 | "launch_in_ci": [ 7 | "Chrome" 8 | ], 9 | "launch_in_dev": [ 10 | "PhantomJS", 11 | "Chrome" 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from './config/environment'; 3 | 4 | const Router = Ember.Router.extend({ 5 | location: config.locationType 6 | }); 7 | 8 | Router.map(function() { 9 | 10 | this.route('homepage'); 11 | }); 12 | 13 | export default Router; 14 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | .DS_Store 13 | /.sass-cache 14 | /connect.lock 15 | /coverage/* 16 | /libpeerconnection.log 17 | npm-debug.log 18 | testem.log 19 | -------------------------------------------------------------------------------- /blueprints/ember-stagger-swagger/index.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 3 | module.exports = { 4 | description: 'ember-stagger-swagger', 5 | 6 | // locals: function(options) { 7 | // // Return custom template variables here. 8 | // return { 9 | // foo: options.entity.options.foo 10 | // }; 11 | // } 12 | 13 | normalizeEntityName: function () {} 14 | 15 | }; 16 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/space-craft-card.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{itemNumber}} 4 | 5 |
6 | 7 |
8 |
9 | {{item.title}} 10 |
11 |
12 | -------------------------------------------------------------------------------- /tests/dummy/app/components/space-craft-card.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { 4 | Component, 5 | computed, 6 | } = Ember; 7 | 8 | export default Component.extend({ 9 | 10 | classNames: [ 11 | 'space-craft-card', 12 | 'o-flex-grid', 13 | 'o-flex-grid--auto', 14 | 'o-flex-grid--noWrap', 15 | ], 16 | 17 | 18 | model: null, 19 | 20 | itemNumber: computed('index', function computeItemNumber() { 21 | return this.get('index') + 1; 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | let App; 7 | 8 | Ember.MODEL_FACTORY_INJECTIONS = true; 9 | 10 | App = Ember.Application.extend({ 11 | modulePrefix: config.modulePrefix, 12 | podModulePrefix: config.podModulePrefix, 13 | Resolver 14 | }); 15 | 16 | loadInitializers(App, config.modulePrefix); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /tests/helpers/integration/get-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the actual DOM node of of the component rendered during an integration 3 | * test, which, initially, will have a parent div wrapping it. 4 | * 5 | * This gives us direct access to the element, and, inherently, the DOM API 6 | */ 7 | export default function getNode (context) { 8 | // allow for context to be passed as a param, otherwise, use the bound "this" 9 | context = context || this; 10 | return context.$()[0].firstElementChild; 11 | } 12 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Application from '../../app'; 3 | import config from '../../config/environment'; 4 | 5 | export default function startApp(attrs) { 6 | let application; 7 | 8 | let attributes = Ember.merge({}, config.APP); 9 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; 10 | 11 | Ember.run(() => { 12 | application = Application.create(attributes); 13 | application.setupForTesting(); 14 | application.injectTestHelpers(); 15 | }); 16 | 17 | return application; 18 | } 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "-Promise" 6 | ], 7 | "browser": true, 8 | "boss": true, 9 | "curly": true, 10 | "debug": false, 11 | "devel": true, 12 | "eqeqeq": true, 13 | "evil": true, 14 | "forin": false, 15 | "immed": false, 16 | "laxbreak": false, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": false, 20 | "nonew": false, 21 | "nomen": false, 22 | "onevar": false, 23 | "plusplus": false, 24 | "regexp": false, 25 | "undef": true, 26 | "sub": true, 27 | "strict": false, 28 | "white": false, 29 | "eqnull": true, 30 | "esnext": true, 31 | "unused": true 32 | } 33 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /tests/dummy/app/components/homepage-demo.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { 4 | Component, 5 | } = Ember; 6 | 7 | export default Component.extend({ 8 | 9 | classNames: ['homepage-demo'], 10 | 11 | staggerDirectionOptions: null, 12 | staggerListItems: null, 13 | currentStaggerDirection: null, 14 | 15 | showItems: false, 16 | 17 | init () { 18 | this._super(...arguments); 19 | 20 | this.currentStaggerDirection = this.currentStaggerDirection || 'left'; 21 | }, 22 | 23 | 24 | actions: { 25 | 26 | onStaggerDirectionSelected (direction) { 27 | this.set('currentStaggerDirection', direction); 28 | }, 29 | 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /config/release.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | // var RSVP = require('rsvp'); 3 | 4 | // For details on each option run `ember help release` 5 | module.exports = { 6 | // local: true, 7 | // remote: 'some_remote', 8 | // annotation: "Release %@", 9 | // message: "Bumped version to %@", 10 | // manifest: [ 'package.json', 'bower.json', 'someconfig.json' ], 11 | // publish: true, 12 | // strategy: 'date', 13 | // format: 'YYYY-MM-DD', 14 | // timezone: 'America/Los_Angeles', 15 | // 16 | // beforeCommit: function(project, versions) { 17 | // return new RSVP.Promise(function(resolve, reject) { 18 | // // Do custom things here... 19 | // }); 20 | // } 21 | }; 22 | -------------------------------------------------------------------------------- /addon/utils/get-animation-prefix.js: -------------------------------------------------------------------------------- 1 | export default function getAnimationPrefix(element) { 2 | 3 | if (element instanceof HTMLElement && element.style) { 4 | 5 | if (typeof element.style.animation !== 'undefined') { 6 | return ''; 7 | } 8 | 9 | if (typeof element.style.webkitAnimation !== 'undefined') { 10 | return 'webkit'; 11 | } 12 | 13 | if (typeof element.style.mozAnimation !== 'undefined') { 14 | return 'moz'; 15 | } 16 | 17 | if (typeof element.style.msAnimation !== 'undefined') { 18 | return 'ms'; 19 | } 20 | 21 | if (typeof element.style.oAnimation !== 'undefined') { 22 | return 'o'; 23 | } 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.js] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.hbs] 21 | insert_final_newline = false 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [*.css] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | [*.html] 30 | indent_style = space 31 | indent_size = 2 32 | 33 | [*.{diff,md}] 34 | trim_trailing_whitespace = false 35 | -------------------------------------------------------------------------------- /tests/helpers/unit/test-defaults-after-invalid-instantiation.js: -------------------------------------------------------------------------------- 1 | /* jshint ignore:start */ 2 | 3 | /** 4 | * Helper function that instantiates an object with invalid property values, 5 | * and tests that the value was set to an expected default. 6 | * 7 | */ 8 | export default function testDefaultsAfterInvalidInstantiation ( 9 | assert, 10 | constructorObj, 11 | propName, 12 | expectedValue, 13 | invalidInputs, 14 | standardObjConfig = {} 15 | ) { 16 | 17 | invalidInputs.forEach((input) => { 18 | subject = constructorObj.create({ ...standardObjConfig, [propName]: input }); 19 | expected = expectedValue; 20 | actual = subject.get(`${propName}`); 21 | assert.equal(actual, expected); 22 | }); 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/homepage.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { Route } = Ember; 4 | 5 | const spaceCrafts = [ 6 | { name: 'Falcon Heavy', weight: 33, size: 99, imgURL: '' }, 7 | { name: 'Falcon Heavy', weight: 33, size: 99, imgURL: '' }, 8 | { name: 'Falcon Heavy', weight: 33, size: 99, imgURL: '' }, 9 | { name: 'Falcon Heavy', weight: 33, size: 99, imgURL: '' }, 10 | { name: 'Falcon Heavy', weight: 33, size: 99, imgURL: '' }, 11 | { name: 'Falcon Heavy', weight: 33, size: 99, imgURL: '' }, 12 | ]; 13 | 14 | export default Route.extend({ 15 | 16 | model () { 17 | return { 18 | staggerDirectionOptions: ['left', 'up', 'right', 'down'], 19 | staggerListItems: spaceCrafts, 20 | }; 21 | }, 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /tests/helpers/module-for-acceptance.js: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | import Ember from 'ember'; 3 | import startApp from '../helpers/start-app'; 4 | import destroyApp from '../helpers/destroy-app'; 5 | 6 | const { RSVP: { Promise } } = Ember; 7 | 8 | 9 | export default function(name, options = {}) { 10 | module(name, { 11 | beforeEach() { 12 | this.application = startApp(); 13 | 14 | if (options.beforeEach) { 15 | return options.beforeEach.apply(this, arguments); 16 | } 17 | }, 18 | 19 | afterEach() { 20 | if (options.afterEach) { 21 | let afterEach = options.afterEach && options.afterEach.apply(this, arguments); 22 | return Promise.resolve(afterEach).then(() => destroyApp(this.application)); 23 | } 24 | } 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | 16 | {{content-for "head-footer"}} 17 | 18 | 19 | 20 | {{content-for "body"}} 21 | 22 | 23 | 24 | 25 | {{content-for "body-footer"}} 26 | 27 | 28 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | /* global require, module */ 3 | 'use strict'; 4 | 5 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 6 | const Funnel = require('broccoli-funnel'); 7 | const path = require('path'); 8 | 9 | const MATCH_CSS = new RegExp('.*\\.css$'); 10 | 11 | module.exports = function(defaults) { 12 | 13 | const app = new EmberAddon(defaults, { 14 | // Add options here 15 | }); 16 | 17 | const stylesDir = path.join(__dirname, 'app', 'styles'); 18 | const stylesTree = new Funnel(stylesDir, { 19 | include: [MATCH_CSS], 20 | destDir: 'vendor' 21 | }); 22 | 23 | 24 | /* 25 | This build file specifies the options for the dummy test app of this 26 | addon, located in `/tests/dummy` 27 | This build file does *not* influence how the addon or the app using it 28 | behave. You most likely want to be modifying `./index.js` or app's build file 29 | */ 30 | 31 | return app.toTree([stylesTree]); 32 | }; 33 | -------------------------------------------------------------------------------- /tests/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "location", 6 | "setTimeout", 7 | "$", 8 | "-Promise", 9 | "define", 10 | "console", 11 | "visit", 12 | "exists", 13 | "fillIn", 14 | "click", 15 | "keyEvent", 16 | "triggerEvent", 17 | "find", 18 | "findWithAssert", 19 | "wait", 20 | "DS", 21 | "andThen", 22 | "currentURL", 23 | "currentPath", 24 | "currentRouteName" 25 | ], 26 | "node": false, 27 | "browser": false, 28 | "boss": true, 29 | "curly": true, 30 | "debug": false, 31 | "devel": false, 32 | "eqeqeq": true, 33 | "evil": true, 34 | "forin": false, 35 | "immed": false, 36 | "laxbreak": false, 37 | "newcap": true, 38 | "noarg": true, 39 | "noempty": false, 40 | "nonew": false, 41 | "nomen": false, 42 | "onevar": false, 43 | "plusplus": false, 44 | "regexp": false, 45 | "undef": true, 46 | "sub": true, 47 | "strict": false, 48 | "white": false, 49 | "eqnull": true, 50 | "esnext": true, 51 | "unused": true 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {{content-for "body-footer"}} 32 | {{content-for "test-body-footer"}} 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | var ENV = { 5 | modulePrefix: 'dummy', 6 | environment: environment, 7 | baseURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | } 14 | }, 15 | 16 | APP: { 17 | // Here you can pass flags/options to your application instance 18 | // when it is created 19 | } 20 | }; 21 | 22 | if (environment === 'development') { 23 | // ENV.APP.LOG_RESOLVER = true; 24 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 25 | // ENV.APP.LOG_TRANSITIONS = true; 26 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 27 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 28 | } 29 | 30 | if (environment === 'test') { 31 | // Testem prefers this... 32 | ENV.baseURL = '/'; 33 | ENV.locationType = 'none'; 34 | 35 | // keep test console output quieter 36 | ENV.APP.LOG_ACTIVE_GENERATION = false; 37 | ENV.APP.LOG_VIEW_LOOKUPS = false; 38 | 39 | ENV.APP.rootElement = '#ember-testing'; 40 | } 41 | 42 | if (environment === 'production') { 43 | ENV.locationType = 'hash'; 44 | ENV.baseURL = '/ember-stagger-swagger/'; 45 | 46 | } 47 | 48 | return ENV; 49 | }; 50 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "4" 5 | - "5" 6 | - "6" 7 | 8 | sudo: required 9 | dist: trusty 10 | 11 | addons: 12 | apt: 13 | sources: 14 | - google-chrome 15 | packages: 16 | - google-chrome-stable 17 | 18 | cache: 19 | directories: 20 | - node_modules 21 | 22 | 23 | env: 24 | - EMBER_TRY_SCENARIO=default 25 | - EMBER_TRY_SCENARIO=ember-1.13 26 | - EMBER_TRY_SCENARIO=ember-2.0 27 | - EMBER_TRY_SCENARIO=ember-2.1 28 | - EMBER_TRY_SCENARIO=ember-2.2 29 | - EMBER_TRY_SCENARIO=ember-2.3 30 | - EMBER_TRY_SCENARIO=ember-2.4 31 | - EMBER_TRY_SCENARIO=ember-2.5 32 | - EMBER_TRY_SCENARIO=ember-2.6 33 | - EMBER_TRY_SCENARIO=ember-release 34 | - ALLOW_DEPRECATIONS=true EMBER_TRY_SCENARIO=ember-beta 35 | - ALLOW_DEPRECATIONS=true EMBER_TRY_SCENARIO=ember-canary 36 | 37 | matrix: 38 | fast_finish: true 39 | allow_failures: 40 | - env: ALLOW_DEPRECATIONS=true EMBER_TRY_SCENARIO=ember-canary 41 | 42 | before_install: 43 | - "export DISPLAY=:99.0" 44 | - "sh -e /etc/init.d/xvfb start" 45 | - npm config set progress false 46 | - npm install -g bower 47 | - npm install -g npm@^3 48 | 49 | 50 | install: 51 | - npm install 52 | - bower install 53 | 54 | script: 55 | # Usually, it's ok to finish the test scenario without reverting 56 | # to the addon's original dependency state, skipping "cleanup". 57 | - ember try $EMBER_TRY_SCENARIO test --skip-cleanup 58 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | const path = require('path'); 5 | 6 | const MATCH_CSS = new RegExp('.*\\.css$'); 7 | 8 | const cssNextOptions = { 9 | browsers: ['last 2 version'], 10 | sourcemap: true, 11 | }; 12 | 13 | module.exports = { 14 | name: 'ember-stagger-swagger', 15 | 16 | isDevelopingAddon () { 17 | return false; 18 | }, 19 | 20 | _getCSSFileName: function () { 21 | return this.name + '.css'; 22 | }, 23 | 24 | _isAddon: function () { 25 | const keywords = this.project.pkg.keywords; 26 | return !!(keywords && ~keywords.indexOf('ember-addon')); 27 | }, 28 | 29 | _isDummyApp () { 30 | return !!( (this._isAddon()) && this.name === 'ember-stagger-swagger'); 31 | }, 32 | 33 | included: function (app) { 34 | this._super.included(app); 35 | 36 | if (this._isDummyApp()) { 37 | app.import(path.join('app/styles', this._getCSSFileName())); 38 | } 39 | 40 | if (!this._isAddon()) { 41 | app.import(path.join('vendor', this._getCSSFileName())); 42 | } 43 | 44 | }, 45 | 46 | 47 | treeForVendor: function (node) { 48 | if (this._isAddon()) { return node; } 49 | 50 | const Funnel = require('broccoli-funnel'); 51 | const mergeTrees = require('broccoli-merge-trees'); 52 | const stylesPath = path.join(this.project.nodeModulesPath, this.name, 'app', 'styles'); 53 | 54 | const cssTree = new Funnel(stylesPath, { include: [MATCH_CSS] }); 55 | 56 | node = node ? mergeTrees([node, cssTree]) : cssTree; 57 | 58 | return node; 59 | } 60 | 61 | }; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-stagger-swagger", 3 | "version": "1.0.12", 4 | "description": "GPU stagger animation for Ember list items", 5 | "directories": { 6 | "doc": "doc", 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "build": "ember build", 11 | "start": "ember server", 12 | "test": "ember try:each" 13 | }, 14 | "repository": "https://github.com/BrianSipple/ember-stagger-swagger", 15 | "engines": { 16 | "node": ">= 4.2.x" 17 | }, 18 | "author": "Brian Sipple <@Brian_Sipple>", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "broccoli-asset-rev": "^2.4.2", 22 | "ember-ajax": "^2.0.1", 23 | "ember-cli": "2.6.0-beta.1", 24 | "ember-cli-app-version": "^1.0.0", 25 | "ember-cli-dependency-checker": "^1.2.0", 26 | "ember-cli-github-pages": "0.0.8", 27 | "ember-cli-htmlbars": "^1.0.3", 28 | "ember-cli-htmlbars-inline-precompile": "^0.3.1", 29 | "ember-cli-inject-live-reload": "^1.4.0", 30 | "ember-cli-qunit": "^1.4.0", 31 | "ember-cli-release": "1.0.0-beta.1", 32 | "ember-cli-sri": "^2.1.0", 33 | "ember-cli-uglify": "^1.2.0", 34 | "ember-disable-prototype-extensions": "^1.1.0", 35 | "ember-disable-proxy-controllers": "^1.0.1", 36 | "ember-export-application-global": "^1.0.5", 37 | "ember-load-initializers": "^0.5.1", 38 | "ember-resolver": "^2.0.3", 39 | "ember-welcome-page": "^1.0.1", 40 | "ember-sinon": "0.5.0", 41 | "ember-sinon-qunit": "1.3.0", 42 | "loader.js": "^4.0.1" 43 | }, 44 | "keywords": [ 45 | "ember-addon", 46 | "ember animation", 47 | "ember animation addon", 48 | "ember animation mixin", 49 | "ember animation component", 50 | "ember animation lists", 51 | "stagger animation", 52 | "list animation" 53 | ], 54 | "dependencies": { 55 | "broccoli-funnel": "1.0.1", 56 | "broccoli-merge-trees": "1.1.1", 57 | "ember-cli-babel": "^5.1.6", 58 | "ember-cli-htmlbars": "^1.0.1" 59 | }, 60 | "ember-addon": { 61 | "configPath": "tests/dummy/config", 62 | "demoURL": "http://www.sipple.io/ember-stagger-swagger-demo/" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.0.12 / 02-06-2016 (dd-mm-yyyy) 2 | ================================ 3 | * Fixed boolean logic when checking for an initial `outEffect` argument in `_resolveInitialEffectAttrs`. (`!!` was 4 | being used instead of `!`, causing an unnecessary warning message to be logged to the console.) 5 | 6 | 1.0.11 / 02-06-2016 7 | ================================ 8 | * Fixed an error in `_setAnimationValuesForItems` where `inDelay` and `outDelay` weren't being accounted for (thanks [@techsoldaten](https://github.com/techsoldaten)) 9 | 10 | 1.0.10 / 15-05-2016 11 | ================================ 12 | * `visibility` is no longer set to `hidden` on items that have yet to be animated in (The only rule now applied is `opacity: 0`). This prevents an issue in Safari where even when toggling `visibility` back to `visible`, items would retain their status of `hidden`. 13 | 14 | 1.0.9 / 20-04-2016 15 | ================================ 16 | * Upgrade to `ember@2.5` and `ember-cli@2.6.0-beta.1` 17 | 18 | 1.0.8 / 20-04-2016 19 | ================================ 20 | * Fix typo in README describing when the `onAnimationStart` action was called. 21 | 22 | 1.0.7 / 09-04-2016 23 | ================================ 24 | * perform short-circuited default setting before warning for invalid arguments. 25 | * use `Ember.Logger.warn` instead of `Ember.warn` 26 | 27 | 1.0.6 / 06-04-2016 28 | ================================ 29 | * hotfix: set `isDevelopingAddon` to false 😳 30 | 31 | 1.0.5 / 04-04-2016 32 | ================================ 33 | * Fix build by using Chrome in TravisCI. 34 | 35 | 1.0.4 / 03-04-2016 36 | ================================ 37 | * Remove dependency adding in blueprint (see: https://github.com/BrianSipple/ember-stagger-swagger/issues/1) 38 | 39 | 1.0.3 / 03-04-2016 40 | ================================ 41 | * Bring keyframes into main css file 42 | * Fixes broken import 43 | * Removes need for postCSS build step. 44 | 45 | 1.0.2 / 03-04-2016 46 | ================================ 47 | * Implement addPackagesToProject inside of the main blueprint `afterInstall` hook so that CSS is properly processed. 48 | 49 | 1.0.1 / 03-04-2016 50 | ================================ 51 | * Minor README fixes. 52 | 53 | 1.0.0 / 03-04-2016 54 | ================================ 55 | * Initial release version. 56 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | scenarios: [ 4 | { 5 | name: 'default', 6 | bower: { 7 | dependencies: { } 8 | } 9 | }, 10 | { 11 | name: 'ember-1.13', 12 | bower: { 13 | dependencies: { 14 | 'ember': '~1.13.0' 15 | }, 16 | resolutions: { 17 | 'ember': '~1.13.0' 18 | } 19 | } 20 | }, 21 | { 22 | name: 'ember-2.0', 23 | bower: { 24 | dependencies: { 25 | 'ember': '~2.0.0' 26 | }, 27 | resolutions: { 28 | 'ember': '~2.0.0' 29 | } 30 | } 31 | }, 32 | { 33 | name: 'ember-2.1', 34 | bower: { 35 | dependencies: { 36 | 'ember': '~2.1.0' 37 | }, 38 | resolutions: { 39 | 'ember': '~2.1.0' 40 | } 41 | } 42 | }, 43 | { 44 | name: 'ember-2.2', 45 | bower: { 46 | dependencies: { 47 | 'ember': '~2.2.0' 48 | }, 49 | resolutions: { 50 | 'ember': '~2.2.0' 51 | } 52 | } 53 | }, 54 | { 55 | name: 'ember-2.3', 56 | bower: { 57 | dependencies: { 58 | 'ember': '~2.3.0' 59 | }, 60 | resolutions: { 61 | 'ember': '~2.3.0' 62 | } 63 | } 64 | }, 65 | { 66 | name: 'ember-2.4', 67 | bower: { 68 | dependencies: { 69 | 'ember': '~2.4.0' 70 | }, 71 | resolutions: { 72 | 'ember': '~2.4.0' 73 | } 74 | } 75 | }, 76 | { 77 | name: 'ember-2.5', 78 | bower: { 79 | dependencies: { 80 | 'ember': '~2.5.0' 81 | }, 82 | resolutions: { 83 | 'ember': '~2.5.0' 84 | } 85 | } 86 | }, 87 | { 88 | name: 'ember-2.6', 89 | bower: { 90 | dependencies: { 91 | 'ember': '~2.6.0' 92 | }, 93 | resolutions: { 94 | 'ember': '~2.6.0' 95 | } 96 | } 97 | }, 98 | { 99 | name: 'ember-release', 100 | bower: { 101 | dependencies: { 102 | 'ember': 'components/ember#release' 103 | }, 104 | resolutions: { 105 | 'ember': 'release' 106 | } 107 | } 108 | }, 109 | { 110 | name: 'ember-beta', 111 | bower: { 112 | dependencies: { 113 | 'ember': 'components/ember#beta' 114 | }, 115 | resolutions: { 116 | 'ember': 'beta' 117 | } 118 | } 119 | }, 120 | { 121 | name: 'ember-canary', 122 | bower: { 123 | dependencies: { 124 | 'ember': 'components/ember#canary' 125 | }, 126 | resolutions: { 127 | 'ember': 'canary' 128 | } 129 | } 130 | } 131 | ] 132 | }; 133 | -------------------------------------------------------------------------------- /config/constants/constants.js: -------------------------------------------------------------------------------- 1 | const DEFAULTS = { 2 | 3 | /** 4 | * 2 frames per item (1 frame @ 60fps ~= 16ms) creates a noticeably staggered 5 | * but still-perceptively fluid motion. 6 | * (see: https://en.wikipedia.org/wiki/Traditional_animation#.22Shooting_on_twos.22) 7 | */ 8 | STAGGER_INTERVAL_MS: 32, 9 | 10 | ANIMATION_DELAY_IN: 0, 11 | ANIMATION_DELAY_OUT: 0, 12 | 13 | ANIMATION_DURATION_IN: 500, 14 | ANIMATION_DURATION_OUT: 500, 15 | 16 | TIMING_FUNCTION_IN: 'cubic-bezier(0.215, 0.610, 0.355, 1.000)', // ease-out-cubic 17 | TIMING_FUNCTION_OUT: 'cubic-bezier(0.55, 0.055, 0.675, 0.19)', // ease-in-cubic 18 | }; 19 | 20 | const foo = { 21 | 22 | }; 23 | 24 | const ANIMATION_DIRECTIONS = { 25 | LEFT: 'left', 26 | DOWN: 'down', 27 | RIGHT: 'right', 28 | UP: 'up', 29 | }; 30 | 31 | const ANIMATION_NAMES = { 32 | SLIDE_AND_FADE: 'slideAndFade', 33 | SLIDE: 'slide', 34 | FADE: 'fade', 35 | SCALE: 'scale', 36 | }; 37 | 38 | const KEYFRAMES_MAP = { 39 | [ANIMATION_DIRECTIONS.LEFT]: { 40 | in: { 41 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeInFromRight', 42 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideInFromRight', 43 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeIn', 44 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleIn', 45 | }, 46 | out: { 47 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeOutLeft', 48 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideOutLeft', 49 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeOut', 50 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleOut', 51 | }, 52 | }, 53 | [ANIMATION_DIRECTIONS.DOWN]: { 54 | in: { 55 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeInFromTop', 56 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideInFromTop', 57 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeIn', 58 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleIn', 59 | }, 60 | out: { 61 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeOutDown', 62 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideOutDown', 63 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeOut', 64 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleOut', 65 | }, 66 | }, 67 | [ANIMATION_DIRECTIONS.RIGHT]: { 68 | in: { 69 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeInFromLeft', 70 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideInFromLeft', 71 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeIn', 72 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleIn', 73 | }, 74 | out: { 75 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeOutRight', 76 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideOutRight', 77 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeOut', 78 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleOut', 79 | }, 80 | }, 81 | [ANIMATION_DIRECTIONS.UP]: { 82 | in: { 83 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeInFromBottom', 84 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideInFromBottom', 85 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeIn', 86 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleIn', 87 | }, 88 | out: { 89 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeOutUp', 90 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideOutUp', 91 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeOut', 92 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleOut', 93 | }, 94 | }, 95 | }; 96 | 97 | 98 | export default { 99 | foo, 100 | b, 101 | DEFAULTS: DEFAULTS, 102 | ANIMATION_DIRECTIONS, 103 | ANIMATION_NAMES, 104 | KEYFRAMES_MAP, 105 | }; 106 | -------------------------------------------------------------------------------- /addon/constants/constants.js: -------------------------------------------------------------------------------- 1 | const CLASS_NAMES = { 2 | untoggled: 'hasnt-entered', 3 | listItemCompletedInitialToggle: 'completed-initial-enter', 4 | }; 5 | 6 | const DEFAULTS = { 7 | 8 | /** 9 | * 2 frames per item (1 frame @ 60fps ~= 16ms) creates a noticeably staggered 10 | * but still-perceptively fluid motion. 11 | * (see: https://en.wikipedia.org/wiki/Traditional_animation#.22Shooting_on_twos.22) 12 | */ 13 | STAGGER_INTERVAL_MS: 32, 14 | 15 | ANIMATION_DELAY_IN: 0, 16 | ANIMATION_DELAY_OUT: 0, 17 | 18 | ANIMATION_DURATION_IN: 500, 19 | ANIMATION_DURATION_OUT: 500, 20 | 21 | TIMING_FUNCTION_IN: 'cubic-bezier(0.215, 0.610, 0.355, 1.000)', // ease-out-cubic 22 | TIMING_FUNCTION_OUT: 'cubic-bezier(0.55, 0.055, 0.675, 0.19)', // ease-in-cubic 23 | }; 24 | 25 | const ANIMATION_DIRECTIONS = { 26 | LEFT: 'left', 27 | DOWN: 'down', 28 | RIGHT: 'right', 29 | UP: 'up', 30 | }; 31 | 32 | const ANIMATION_NAMES = { 33 | SLIDE_AND_FADE: 'slideAndFade', 34 | SLIDE: 'slide', 35 | FADE: 'fade', 36 | SCALE: 'scale', 37 | }; 38 | 39 | const KEYFRAMES_MAP = { 40 | [ANIMATION_DIRECTIONS.LEFT]: { 41 | in: { 42 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeInFromRight', 43 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideInFromRight', 44 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeIn', 45 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleIn', 46 | }, 47 | out: { 48 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeOutLeft', 49 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideOutLeft', 50 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeOut', 51 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleOut', 52 | }, 53 | }, 54 | [ANIMATION_DIRECTIONS.DOWN]: { 55 | in: { 56 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeInFromTop', 57 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideInFromTop', 58 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeIn', 59 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleIn', 60 | }, 61 | out: { 62 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeOutDown', 63 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideOutDown', 64 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeOut', 65 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleOut', 66 | }, 67 | }, 68 | [ANIMATION_DIRECTIONS.RIGHT]: { 69 | in: { 70 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeInFromLeft', 71 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideInFromLeft', 72 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeIn', 73 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleIn', 74 | }, 75 | out: { 76 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeOutRight', 77 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideOutRight', 78 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeOut', 79 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleOut', 80 | }, 81 | }, 82 | [ANIMATION_DIRECTIONS.UP]: { 83 | in: { 84 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeInFromBottom', 85 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideInFromBottom', 86 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeIn', 87 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleIn', 88 | }, 89 | out: { 90 | [ANIMATION_NAMES.SLIDE_AND_FADE]: '__EmberStaggerSwagger__SlideAndFadeOutUp', 91 | [ANIMATION_NAMES.SLIDE]: '__EmberStaggerSwagger__SlideOutUp', 92 | [ANIMATION_NAMES.FADE]: '__EmberStaggerSwagger__FadeOut', 93 | [ANIMATION_NAMES.SCALE]: '__EmberStaggerSwagger__ScaleOut', 94 | }, 95 | }, 96 | }; 97 | 98 | 99 | export default { 100 | CLASS_NAMES, 101 | DEFAULTS, 102 | ANIMATION_DIRECTIONS, 103 | ANIMATION_NAMES, 104 | KEYFRAMES_MAP, 105 | }; 106 | -------------------------------------------------------------------------------- /tests/integration/components/stagger-set-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { moduleForComponent, test } from 'ember-qunit'; 3 | import { skip } from 'qunit'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | import getNode from '../../helpers/integration/get-node'; 6 | import Constants from 'ember-stagger-swagger/constants/constants'; 7 | 8 | const { 9 | run, 10 | set, 11 | } = Ember; 12 | 13 | const { 14 | ANIMATION_DIRECTIONS, 15 | ANIMATION_NAMES, 16 | KEYFRAMES_MAP, 17 | } = Constants; 18 | 19 | const DataDown = Ember.Object.extend({ 20 | showItems: false, 21 | inDirection: ANIMATION_DIRECTIONS.RIGHT, 22 | inEffect: ANIMATION_NAMES.SLIDE_AND_FADE, 23 | }); 24 | 25 | function renderMinimalContent () { 26 | this.render(hbs` 27 | {{#stagger-set inDirection="left" inEffect="slideAndFade"}} 28 | Seattle 29 | {{/stagger-set}} 30 | `); 31 | } 32 | 33 | 34 | let dataDown; 35 | let actual, expected; 36 | 37 | moduleForComponent('stagger-set', 'Integration | Component | stagger set', { 38 | integration: true 39 | }); 40 | 41 | test('rendering block content', function(assert) { 42 | // Set any properties with this.set('myProperty', 'value'); 43 | // Handle any actions with this.on('myAction', function(val) { ... }); 44 | renderMinimalContent.call(this); 45 | 46 | expected = 'Seattle'; 47 | actual = getNode(this).textContent.trim(); 48 | assert.equal(actual, expected); 49 | }); 50 | 51 | 52 | skip(`broadcasting animation start and animation completion`, function (assert) { 53 | 54 | function onAnimationStart (event) { 55 | expected = true; 56 | actual = event instanceof window.AnimationEvent; 57 | assert.equal(actual, expected); 58 | 59 | expected = 'animationstart'; 60 | actual = event.type; 61 | assert.equal(actual, expected); 62 | } 63 | 64 | function onAnimationComplete (event) { 65 | expected = true; 66 | actual = event instanceof window.AnimationEvent; 67 | assert.equal(actual, expected); 68 | 69 | expected = 'animationend'; 70 | actual = event.type; 71 | assert.equal(actual, expected); 72 | } 73 | 74 | dataDown = DataDown.create({ showItems: false }); 75 | 76 | this.set('onAnimationComplete', onAnimationComplete); 77 | this.set('onAnimationStart', onAnimationStart); 78 | this.set('dataDown', dataDown); 79 | 80 | run(() => { 81 | this.render(hbs` 82 | {{#stagger-set 83 | showItems=dataDown.showItems 84 | inEffect=dataDown.inEffect 85 | inDirection=dataDown.inDirection 86 | onAnimationStart=onAnimationStart 87 | onAnimationComplete=onAnimationComplete 88 | }} 89 |
  • Seattle
  • 90 |
  • New York City
  • 91 |
  • Boston
  • 92 | {{/stagger-set}} 93 | `); 94 | }); 95 | }); 96 | 97 | 98 | test(`toggling the element's animation name when the 99 | value of the \`showItems\` attribute changes after 100 | the initial render`, function (assert) { 101 | 102 | const inEffect = ANIMATION_NAMES.FADE; 103 | const inDirection = ANIMATION_DIRECTIONS.DOWN; 104 | const dataDown = DataDown.create({ showItems: false, inEffect, inDirection }); 105 | 106 | this.set('dataDown', dataDown); 107 | 108 | this.render(hbs` 109 | {{#stagger-set 110 | inEffect=dataDown.inEffect 111 | inDirection=dataDown.inDirection 112 | showItems=dataDown.showItems 113 | }} 114 |
  • Seattle
  • 115 |
  • New York City
  • 116 |
  • Boston
  • 117 | {{/stagger-set}} 118 | `); 119 | 120 | expected = ''; 121 | actual = getNode(this).children[0].style.animationName; 122 | assert.equal(actual, expected); 123 | 124 | run(() => { 125 | set(dataDown, 'showItems', true); 126 | }); 127 | 128 | expected = KEYFRAMES_MAP[inDirection].in[inEffect]; 129 | actual = getNode(this).children[0].style.animationName; 130 | assert.equal(actual, expected); 131 | }); 132 | 133 | 134 | test(`mapping keyframes according to the current direction and effect name`, function (assert) { 135 | 136 | const dataDown = DataDown.create({ outEffect: null, showItems: false }); 137 | this.set('dataDown', dataDown); 138 | 139 | 140 | Object.keys(ANIMATION_DIRECTIONS).forEach((animationDirection) => { 141 | 142 | Object.keys(ANIMATION_NAMES).forEach((animationName) => { 143 | 144 | run(() => { 145 | set(dataDown, 'inEffect', ANIMATION_NAMES[animationName]); 146 | set(dataDown, 'inDirection', ANIMATION_DIRECTIONS[animationDirection]); 147 | set(dataDown, 'showItems', true); 148 | }); 149 | 150 | this.render(hbs` 151 | {{#stagger-set 152 | inDirection=dataDown.inDirection 153 | inEffect=dataDown.inEffect 154 | outEffect=dataDown.outEffect 155 | showItems=dataDown.showItems 156 | }} 157 |
  • Seattle
  • 158 |
  • New York City
  • 159 |
  • Boston
  • 160 | {{/stagger-set}} 161 | `); 162 | 163 | 164 | expected = KEYFRAMES_MAP[ANIMATION_DIRECTIONS[animationDirection]].in[ANIMATION_NAMES[animationName]]; 165 | actual = getNode(this).children[0].style.animationName; 166 | assert.equal(actual, expected); 167 | 168 | }); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/homepage-demo.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |

    5 | Ember Stagger Swagger 6 |

    7 |
    8 | 9 |
    10 | 11 |
    12 | 13 |

    14 | Toggle Stagger Options 15 |

    16 | 17 |
    22 | 23 | {{!-- showItemsToggle column --}} 24 |
    34 | 35 | 36 | {{!-- show/hide toggle --}} 37 |
    38 | 39 |
    40 | 41 | {{input class="o-grid-cell" id="showItemsRadio" type="radio" name="showItemsToggleGroup" value=(mut showItems)}} 42 |
    43 | 44 |
    45 | 46 | {{input class="o-grid-cell" id="hideItemsRadio" type="radio" name="showItemsToggleGroup" value=(mut showItems)}} 47 |
    48 | 49 |
    50 | 51 | {{!-- example in/out keyframes to choose --}} 52 |
    53 |

    Custom Keyframes Examples

    54 | 55 |
    56 | 57 | {{input class="o-grid-cell" id="keyframes--slide" type="radio" name="keyframesToggleGroup" value=(mut selectedKeyframe)}} 58 |
    59 | 60 |
    61 | 62 | {{input class="o-grid-cell" id="keyframes--slide-and-fade" type="radio" name="keyframesToggleGroup" value=(mut selectedKeyframe)}} 63 |
    64 | 65 |
    66 | 67 | {{input class="o-grid-cell" id="keyframes--fad" type="radio" name="keyframesToggleGroup" value=(mut selectedKeyframe)}} 68 |
    69 | 70 |
    71 | 72 | {{input class="o-grid-cell" id="keyframes--scale" type="radio" name="keyframesToggleGroup" value=(mut selectedKeyframe)}} 73 |
    74 | 75 |
    76 | 77 |
    78 | 79 | {{!-- Content column --}} 80 |
    84 | 85 |
    90 | 91 | {{#stagger-list class="g-list-reset" tagName="ul" staggerDirection="left"}} 92 | {{#each staggerListItems as |item index|}} 93 | {{space-craft-card tagName="li" class="g-box-shadow-2 g-nearWhite u-m2" item=item index=index}} 94 | {{/each}} 95 | {{/stagger-list}} 96 |
    97 | 98 | 99 | {{!-- Direction menu TODO: Make THIS a stagger list 😀? --}} 100 | 130 | 131 |
    132 | 133 |
    134 | 135 |
    136 | 137 | 138 |
    139 | 140 |
    141 | -------------------------------------------------------------------------------- /tests/unit/mixins/stagger-set-test.js: -------------------------------------------------------------------------------- 1 | /* jshint ignore:start */ 2 | 3 | import Ember from 'ember'; 4 | import StaggerSetMixin from 'ember-stagger-swagger/mixins/stagger-set'; 5 | import { module, test } from 'qunit'; 6 | import Constants from 'ember-stagger-swagger/constants/constants'; 7 | import testDefaultsAfterInvalidInstantiation from '../../helpers/unit/test-defaults-after-invalid-instantiation'; 8 | 9 | const { 10 | ANIMATION_DIRECTIONS, 11 | ANIMATION_NAMES, 12 | KEYFRAMES_MAP, 13 | DEFAULTS, 14 | } = Constants; 15 | 16 | const minimalConfig = { 17 | inDirection: ANIMATION_DIRECTIONS.LEFT, 18 | inEffect: ANIMATION_NAMES.SLIDE_AND_FADE, 19 | }; 20 | 21 | 22 | let StaggerSetObject, subject; 23 | let actual, expected; 24 | 25 | module('Unit | Mixin | stagger list'); 26 | 27 | test('requiring a valid `inDirection`', function (assert) { 28 | assert.expect(3); 29 | StaggerSetObject = Ember.Object.extend(StaggerSetMixin); 30 | 31 | assert.throws( 32 | () => { subject = StaggerSetObject.create({ inDirection: null }); }, 33 | /inDirection/, 34 | 'failing to specify a `inDirection` throws an error' 35 | ); 36 | 37 | assert.throws( 38 | () => { subject = StaggerSetObject.create({ inDirection: 'spiral' }); }, 39 | /inDirection/, 40 | 'specifying an invalid `inDirection` throws an error' 41 | ); 42 | 43 | assert.ok(subject = StaggerSetObject.create(minimalConfig)); 44 | }); 45 | 46 | test('requiring a valid `inEffect`', function (assert) { 47 | assert.expect(3); 48 | StaggerSetObject = Ember.Object.extend(StaggerSetMixin); 49 | 50 | assert.throws( 51 | () => { 52 | subject = StaggerSetObject.create({ 53 | inDirection: ANIMATION_DIRECTIONS.LEFT, 54 | inEffect: null, 55 | }); 56 | }, 57 | /inEffect/, 58 | 'failing to specify a `inEffect` throws an error' 59 | ); 60 | 61 | assert.throws( 62 | () => { 63 | subject = StaggerSetObject.create({ 64 | inDirection: ANIMATION_DIRECTIONS.LEFT, 65 | inEffect: 'supernova', 66 | }); 67 | }, 68 | 'specifying an invalid `inEffect` throws an error' 69 | ); 70 | 71 | assert.ok(subject = StaggerSetObject.create(minimalConfig)); 72 | }); 73 | 74 | 75 | test(`\`staggerInterval\` defaults to ${DEFAULTS.STAGGER_INTERVAL_MS} when it's 76 | not set to a value greater than or equal to 32`, function (assert) { 77 | assert.expect(4); 78 | 79 | StaggerSetObject = Ember.Object.extend(StaggerSetMixin); 80 | 81 | const valid = 90; 82 | const invalidNaN = 'superfast'; 83 | const invalidNumber = -900; 84 | const invalidNumber2 = 31; 85 | 86 | subject = StaggerSetObject.create({ ...minimalConfig, staggerInterval: valid }); 87 | expected = valid; 88 | actual = subject.get('staggerInterval'); 89 | assert.equal(actual, expected); 90 | 91 | expected = DEFAULTS.STAGGER_INTERVAL_MS; 92 | 93 | testDefaultsAfterInvalidInstantiation( 94 | assert, 95 | StaggerSetObject, 96 | 'staggerInterval', 97 | expected, 98 | [invalidNaN, invalidNumber, invalidNumber2], 99 | minimalConfig 100 | ); 101 | }); 102 | 103 | test(`\`inDelay\` defaults to ${DEFAULTS.ANIMATION_DELAY_IN} if not set 104 | to a number greater than 0`, function (assert) { 105 | assert.expect(3); 106 | 107 | StaggerSetObject = Ember.Object.extend(StaggerSetMixin); 108 | 109 | const valid = 90; 110 | const invalidNaN = 'superfast'; 111 | const invalidNumber = -900; 112 | 113 | subject = StaggerSetObject.create({...minimalConfig, inDelay: valid }); 114 | expected = valid; 115 | actual = subject.get('inDelay'); 116 | assert.equal(actual, expected); 117 | 118 | expected = DEFAULTS.ANIMATION_DELAY_IN; 119 | 120 | testDefaultsAfterInvalidInstantiation( 121 | assert, 122 | StaggerSetObject, 123 | 'inDelay', 124 | expected, 125 | [invalidNaN, invalidNumber], 126 | minimalConfig 127 | ); 128 | }); 129 | 130 | test(`\`outDelay\` defaults to matching \`inDelay\` if not set 131 | to a number greater than 0`, function (assert) { 132 | 133 | assert.expect(3); 134 | 135 | StaggerSetObject = Ember.Object.extend(StaggerSetMixin); 136 | 137 | const valid = 90; 138 | const invalidNaN = 'superfast'; 139 | const invalidNumber = -900; 140 | 141 | subject = StaggerSetObject.create({...minimalConfig, outDelay: valid }); 142 | expected = valid; 143 | actual = subject.get('outDelay'); 144 | assert.equal(actual, expected); 145 | 146 | expected = DEFAULTS.ANIMATION_DELAY_IN; 147 | 148 | testDefaultsAfterInvalidInstantiation( 149 | assert, 150 | StaggerSetObject, 151 | 'outDelay', 152 | expected, 153 | [invalidNaN, invalidNumber], 154 | minimalConfig 155 | ); 156 | }); 157 | 158 | test(`\`inDuration\` defaults to ${DEFAULTS.ANIMATION_DURATION_IN} if not set 159 | to a number greater than 0`, function (assert) { 160 | 161 | assert.expect(4); 162 | 163 | StaggerSetObject = Ember.Object.extend(StaggerSetMixin); 164 | 165 | const valid = 90; 166 | const invalidNaN = 'superfast'; 167 | const invalidNumber = 0; 168 | const invalidNumber2 = -900; 169 | 170 | subject = StaggerSetObject.create({...minimalConfig, inDuration: valid }); 171 | expected = valid; 172 | actual = subject.get('inDuration'); 173 | assert.equal(actual, expected); 174 | 175 | expected = DEFAULTS.ANIMATION_DURATION_IN; 176 | 177 | testDefaultsAfterInvalidInstantiation( 178 | assert, 179 | StaggerSetObject, 180 | 'inDuration', 181 | expected, 182 | [invalidNaN, invalidNumber, invalidNumber2], 183 | minimalConfig 184 | ); 185 | }); 186 | 187 | test(`\`outDuration\` defaults to matching \`inDuration\` if not set 188 | to a number greater than 0`, function (assert) { 189 | 190 | assert.expect(4); 191 | 192 | StaggerSetObject = Ember.Object.extend(StaggerSetMixin); 193 | 194 | const valid = 90; 195 | const invalidNaN = 'superfast'; 196 | const invalidNumber = 0; 197 | const invalidNumber2 = -900; 198 | 199 | subject = StaggerSetObject.create({...minimalConfig, outDuration: valid }); 200 | expected = valid; 201 | actual = subject.get('outDuration'); 202 | assert.equal(actual, expected); 203 | 204 | expected = DEFAULTS.ANIMATION_DURATION_IN; 205 | 206 | testDefaultsAfterInvalidInstantiation( 207 | assert, 208 | StaggerSetObject, 209 | 'outDuration', 210 | expected, 211 | [invalidNaN, invalidNumber, invalidNumber2], 212 | minimalConfig 213 | ); 214 | }); 215 | 216 | test(`\`inTimingFunc\` defaults to ${DEFAULTS.TIMING_FUNCTION_IN} if not set`, function (assert) { 217 | assert.expect(2); 218 | StaggerSetObject = Ember.Object.extend(StaggerSetMixin); 219 | expected = DEFAULTS.TIMING_FUNCTION_IN; 220 | 221 | subject = StaggerSetObject.create({...minimalConfig, inTimingFunc: DEFAULTS.TIMING_FUNCTION_IN }); 222 | actual = subject.get('inTimingFunc'); 223 | assert.equal(actual, expected); 224 | 225 | subject = StaggerSetObject.create({...minimalConfig, inTimingFunc: null }); 226 | actual = subject.get('inTimingFunc'); 227 | assert.equal(actual, expected); 228 | }); 229 | 230 | test(`\`outTimingFunc\` defaults to ${DEFAULTS.TIMING_FUNCTION_OUT} if not set`, function (assert) { 231 | assert.expect(2); 232 | StaggerSetObject = Ember.Object.extend(StaggerSetMixin); 233 | expected = DEFAULTS.TIMING_FUNCTION_OUT; 234 | 235 | subject = StaggerSetObject.create({...minimalConfig, outTimingFunc: DEFAULTS.TIMING_FUNCTION_OUT }); 236 | actual = subject.get('outTimingFunc'); 237 | assert.equal(actual, expected); 238 | 239 | subject = StaggerSetObject.create({...minimalConfig, outTimingFunc: null }); 240 | actual = subject.get('outTimingFunc'); 241 | assert.equal(actual, expected); 242 | }); 243 | 244 | test(`\`outDirection\` defaults to matching \`inDirection\` if not set`, function (assert) { 245 | assert.expect(1); 246 | StaggerSetObject = Ember.Object.extend(StaggerSetMixin); 247 | 248 | inDirection = ANIMATION_DIRECTIONS.LEFT; 249 | subject = StaggerSetObject.create({...minimalConfig, inDirection, outDirection: null }); 250 | 251 | expected = inDirection; 252 | actual = subject.get('outDirection'); 253 | assert.equal(actual, expected); 254 | }); 255 | 256 | test(`\`outEffect\` defaults to matching \`inEffect\` if not set`, function (assert) { 257 | assert.expect(1); 258 | StaggerSetObject = Ember.Object.extend(StaggerSetMixin); 259 | 260 | inEffect = ANIMATION_NAMES.SLIDE_AND_FADE; 261 | subject = StaggerSetObject.create({...minimalConfig, inEffect, outEffect: null }); 262 | 263 | expected = inEffect; 264 | actual = subject.get('inEffect'); 265 | assert.equal(actual, expected); 266 | }); 267 | -------------------------------------------------------------------------------- /app/styles/ember-stagger-swagger.css: -------------------------------------------------------------------------------- 1 | /* TODO: JS all the things? */ 2 | .__ember-stagger-swagger__stagger-set.hasnt-entered > :not(.completed-initial-enter) { 3 | opacity: 0; 4 | } 5 | 6 | 7 | 8 | /* --------------------- KEYFRAMES --------------------- */ 9 | 10 | 11 | /** 12 | * --------------------------------------------------------------- 13 | * INs 14 | * --------------------------------------------------------------- 15 | */ 16 | 17 | /** 18 | * SlideAndFade 19 | */ 20 | @keyframes __EmberStaggerSwagger__SlideAndFadeInFromLeft { 21 | 0% { 22 | opacity: 0; 23 | visibility: visible; 24 | transform: translate3d(-100%, 0, 0); 25 | } 26 | 100% { 27 | opacity: 1; 28 | transform: translate3d(0, 0, 0); 29 | } 30 | } 31 | @-webkit-keyframes __EmberStaggerSwagger__SlideAndFadeInFromLeft { 32 | 0% { 33 | opacity: 0; 34 | visibility: visible; 35 | -webkit-transform: translate3d(-100%, 0, 0); 36 | } 37 | 100% { 38 | opacity: 1; 39 | -webkit-transform: translate3d(0, 0, 0); 40 | } 41 | } 42 | 43 | 44 | @keyframes __EmberStaggerSwagger__SlideAndFadeInFromRight { 45 | 0% { 46 | opacity: 0; 47 | visibility: visible; 48 | transform: translate3d(100%, 0, 0); 49 | } 50 | 100% { 51 | opacity: 1; 52 | transform: translate3d(0, 0, 0); 53 | } 54 | } 55 | @-webkit-keyframes __EmberStaggerSwagger__SlideAndFadeInFromRight { 56 | 0% { 57 | opacity: 0; 58 | visibility: visible; 59 | transform: translate3d(100%, 0, 0); 60 | } 61 | 100% { 62 | opacity: 1; 63 | transform: translate3d(0, 0, 0); 64 | } 65 | } 66 | 67 | 68 | @keyframes __EmberStaggerSwagger__SlideAndFadeInFromTop { 69 | 0% { 70 | opacity: 0; 71 | visibility: visible; 72 | transform: translate3d(0, -100%, 0); 73 | } 74 | 100% { 75 | opacity: 1; 76 | transform: translate3d(0, 0, 0); 77 | } 78 | } 79 | @-webkit-keyframes __EmberStaggerSwagger__SlideAndFadeInFromTop { 80 | 0% { 81 | opacity: 0; 82 | visibility: visible; 83 | -webkit-transform: translate3d(0, -100%, 0); 84 | } 85 | 100% { 86 | opacity: 1; 87 | -webkit-transform: translate3d(0, 0, 0); 88 | } 89 | } 90 | 91 | 92 | @keyframes __EmberStaggerSwagger__SlideAndFadeInFromBottom { 93 | 0% { 94 | opacity: 0; 95 | visibility: visible; 96 | transform: translate3d(0, 100%, 0); 97 | } 98 | 100% { 99 | opacity: 1; 100 | transform: translate3d(0, 0, 0); 101 | } 102 | } 103 | @-webkit-keyframes __EmberStaggerSwagger__SlideAndFadeInFromBottom { 104 | 0% { 105 | opacity: 0; 106 | visibility: visible; 107 | -webkit-transform: translate3d(0, 100%, 0); 108 | } 109 | 100% { 110 | opacity: 1; 111 | -webkit-transform: translate3d(0, 0, 0); 112 | } 113 | } 114 | 115 | 116 | 117 | /** 118 | * Slide 119 | */ 120 | @keyframes __EmberStaggerSwagger__SlideInFromLeft { 121 | 0% { 122 | opacity: 1; 123 | visibility: visible; 124 | transform: translate3d(-100%, 0, 0); 125 | } 126 | 100% { 127 | transform: translate3d(0, 0, 0); 128 | } 129 | } 130 | @-webkit-keyframes __EmberStaggerSwagger__SlideInFromLeft { 131 | 0% { 132 | opacity: 1; 133 | visibility: visible; 134 | -webkit-transform: translate3d(-100%, 0, 0); 135 | } 136 | 100% { 137 | -webkit-transform: translate3d(0, 0, 0); 138 | } 139 | } 140 | 141 | 142 | @keyframes __EmberStaggerSwagger__SlideInFromRight { 143 | 0% { 144 | opacity: 1; 145 | visibility: visible; 146 | transform: translate3d(100%, 0, 0); 147 | } 148 | 100% { 149 | transform: translate3d(0, 0, 0); 150 | } 151 | } 152 | @-webkit-keyframes __EmberStaggerSwagger__SlideInFromRight { 153 | 0% { 154 | opacity: 1; 155 | visibility: visible; 156 | -webkit-transform: translate3d(100%, 0, 0); 157 | } 158 | 100% { 159 | -webkit-transform: translate3d(0, 0, 0); 160 | } 161 | } 162 | 163 | 164 | @keyframes __EmberStaggerSwagger__SlideInFromTop { 165 | 0% { 166 | opacity: 1; 167 | visibility: visible; 168 | transform: translate3d(0, -100%, 0); 169 | } 170 | 100% { 171 | transform: translate3d(0, 0, 0); 172 | } 173 | } 174 | @-webkit-keyframes __EmberStaggerSwagger__SlideInFromTop { 175 | 0% { 176 | opacity: 1; 177 | visibility: visible; 178 | -webkit-transform: translate3d(0, -100%, 0); 179 | } 180 | 100% { 181 | -webkit-transform: translate3d(0, 0, 0); 182 | } 183 | } 184 | 185 | 186 | @keyframes __EmberStaggerSwagger__SlideInFromBottom { 187 | 0% { 188 | opacity: 1; 189 | visibility: visible; 190 | transform: translate3d(0, 100%, 0); 191 | } 192 | 100% { 193 | transform: translate3d(0, 0, 0); 194 | } 195 | } 196 | @-webkit-keyframes __EmberStaggerSwagger__SlideInFromBottom { 197 | 0% { 198 | opacity: 1; 199 | visibility: visible; 200 | -webkit-transform: translate3d(0, 100%, 0); 201 | } 202 | 100% { 203 | -webkit-transform: translate3d(0, 0, 0); 204 | } 205 | } 206 | 207 | 208 | 209 | /** 210 | * Fade 211 | */ 212 | @keyframes __EmberStaggerSwagger__FadeIn { 213 | 0% { 214 | visibility: visible; 215 | opacity: 0; 216 | } 217 | 100% { 218 | opacity: 1; 219 | } 220 | } 221 | @-webkit-keyframes __EmberStaggerSwagger__FadeIn { 222 | 0% { 223 | visibility: visible; 224 | opacity: 0; 225 | } 226 | 100% { 227 | opacity: 1; 228 | } 229 | } 230 | 231 | 232 | 233 | /** 234 | * Scale 235 | */ 236 | @keyframes __EmberStaggerSwagger__ScaleIn { 237 | 0% { 238 | visibility: visible; 239 | opacity: 1; 240 | transform: scale(0); 241 | } 242 | 100% { 243 | opacity: 1; 244 | transform: scale(1); 245 | } 246 | } 247 | @-webkit-keyframes __EmberStaggerSwagger__ScaleIn { 248 | 0% { 249 | visibility: visible; 250 | opacity: 1; 251 | -webkit-transform: scale(0); 252 | } 253 | 100% { 254 | opacity: 1; 255 | -webkit-transform: scale(1); 256 | } 257 | } 258 | 259 | 260 | 261 | 262 | 263 | 264 | /** 265 | * --------------------------------------------------------------- 266 | * OUTs 267 | * --------------------------------------------------------------- 268 | */ 269 | 270 | /** 271 | * SlideAndFade 272 | */ 273 | @keyframes __EmberStaggerSwagger__SlideAndFadeOutRight { 274 | 0% { 275 | opacity: 1; 276 | transform: translate3d(0, 0, 0); 277 | } 278 | 100% { 279 | opacity: 0; 280 | visibility: hidden; 281 | transform: translate3d(100%, 0, 0); 282 | } 283 | } 284 | @-webkit-keyframes __EmberStaggerSwagger__SlideAndFadeOutRight { 285 | 0% { 286 | opacity: 1; 287 | -webkit-transform: translate3d(0, 0, 0); 288 | } 289 | 100% { 290 | opacity: 0; 291 | visibility: hidden; 292 | -webkit-transform: translate3d(100%, 0, 0); 293 | } 294 | } 295 | 296 | 297 | @keyframes __EmberStaggerSwagger__SlideAndFadeOutLeft { 298 | 0% { 299 | opacity: 1; 300 | visibility: visible; 301 | transform: translate3d(0, 0, 0); 302 | } 303 | 100% { 304 | opacity: 0; 305 | visibility: hidden; 306 | transform: translate3d(-100%, 0, 0); 307 | } 308 | } 309 | @-webkit-keyframes __EmberStaggerSwagger__SlideAndFadeOutLeft { 310 | 0% { 311 | opacity: 1; 312 | visibility: visible; 313 | -webkit-transform: translate3d(0, 0, 0); 314 | } 315 | 100% { 316 | opacity: 0; 317 | visibility: hidden; 318 | -webkit-transform: translate3d(-100%, 0, 0); 319 | } 320 | } 321 | 322 | 323 | @keyframes __EmberStaggerSwagger__SlideAndFadeOutDown { 324 | 0% { 325 | opacity: 1; 326 | transform: translate3d(0, 0, 0); 327 | } 328 | 100% { 329 | opacity: 0; 330 | visibility: hidden; 331 | transform: translate3d(0, 100%, 0); 332 | } 333 | } 334 | @-webkit-keyframes __EmberStaggerSwagger__SlideAndFadeOutDown { 335 | 0% { 336 | opacity: 1; 337 | -webkit-transform: translate3d(0, 0, 0); 338 | } 339 | 100% { 340 | opacity: 0; 341 | visibility: hidden; 342 | -webkit-transform: translate3d(0, 100%, 0); 343 | } 344 | } 345 | 346 | 347 | @keyframes __EmberStaggerSwagger__SlideAndFadeOutUp { 348 | 0% { 349 | opacity: 1; 350 | transform: translate3d(0, 0, 0); 351 | } 352 | 100% { 353 | opacity: 0; 354 | visibility: hidden; 355 | transform: translate3d(0, -100%, 0); 356 | } 357 | } 358 | @-webkit-keyframes __EmberStaggerSwagger__SlideAndFadeOutUp { 359 | 0% { 360 | opacity: 1; 361 | -webkit-transform: translate3d(0, 0, 0); 362 | } 363 | 100% { 364 | opacity: 0; 365 | visibility: hidden; 366 | -webkit-transform: translate3d(0, -100%, 0); 367 | } 368 | } 369 | 370 | 371 | 372 | /** 373 | * Slide 374 | */ 375 | @keyframes __EmberStaggerSwagger__SlideOutRight { 376 | 0% { 377 | transform: translate3d(0, 0, 0); 378 | } 379 | 100% { 380 | transform: translate3d(100%, 0, 0); 381 | visibility: hidden; 382 | } 383 | } 384 | @-webkit-keyframes __EmberStaggerSwagger__SlideOutRight { 385 | 0% { 386 | -webkit-transform: translate3d(0, 0, 0); 387 | } 388 | 100% { 389 | -webkit-transform: translate3d(100%, 0, 0); 390 | visibility: hidden; 391 | } 392 | } 393 | 394 | 395 | @keyframes __EmberStaggerSwagger__SlideOutLeft { 396 | 0% { 397 | transform: translate3d(0, 0, 0); 398 | } 399 | 100% { 400 | transform: translate3d(-100%, 0, 0); 401 | visibility: hidden; 402 | } 403 | } 404 | @-webkit-keyframes __EmberStaggerSwagger__SlideOutLeft { 405 | 0% { 406 | -webkit-transform: translate3d(0, 0, 0); 407 | } 408 | 100% { 409 | -webkit-transform: translate3d(-100%, 0, 0); 410 | visibility: hidden; 411 | } 412 | } 413 | 414 | 415 | @keyframes __EmberStaggerSwagger__SlideOutDown { 416 | 0% { 417 | } 418 | 100% { 419 | visibility: hidden; 420 | transform: translate3d(0, 100%, 0); 421 | } 422 | } 423 | @-webkit-keyframes __EmberStaggerSwagger__SlideOutDown { 424 | 0% { 425 | } 426 | 100% { 427 | visibility: hidden; 428 | -webkit-transform: translate3d(0, 100%, 0); 429 | } 430 | } 431 | 432 | 433 | @keyframes __EmberStaggerSwagger__SlideOutUp { 434 | 0% { 435 | transform: translate3d(0, 0, 0); 436 | } 437 | 100% { 438 | visibility: hidden; 439 | transform: translate3d(0, -100%, 0); 440 | } 441 | } 442 | @-webkit-keyframes __EmberStaggerSwagger__SlideOutUp { 443 | 0% { 444 | -webkit-transform: translate3d(0, 0, 0); 445 | } 446 | 100% { 447 | visibility: hidden; 448 | -webkit-transform: translate3d(0, -100%, 0); 449 | } 450 | } 451 | 452 | 453 | 454 | /** 455 | * Fade 456 | */ 457 | @keyframes __EmberStaggerSwagger__FadeOut { 458 | 0% { 459 | opacity: 1; 460 | } 461 | 100% { 462 | visibility: hidden; 463 | opacity: 0; 464 | } 465 | } 466 | @-webkit-keyframes __EmberStaggerSwagger__FadeOut { 467 | 0% { 468 | opacity: 1; 469 | } 470 | 100% { 471 | visibility: hidden; 472 | opacity: 0; 473 | } 474 | } 475 | 476 | 477 | 478 | /** 479 | * Scale 480 | */ 481 | @keyframes __EmberStaggerSwagger__ScaleOut { 482 | 0% { 483 | transform: scale(1); 484 | } 485 | 100% { 486 | visibility: hidden; 487 | transform: scale(0); 488 | } 489 | } 490 | @-webkit-keyframes __EmberStaggerSwagger__ScaleOut { 491 | 0% { 492 | -webkit-transform: scale(1); 493 | } 494 | 100% { 495 | visibility: hidden; 496 | -webkit-transform: scale(0); 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-stagger-swagger 2 | 3 | [![npm version](https://badge.fury.io/js/ember-stagger-swagger.svg)](https://badge.fury.io/js/ember-stagger-swagger) [![Build Status](https://travis-ci.org/BrianSipple/ember-stagger-swagger.svg?branch=master)](https://travis-ci.org/BrianSipple/ember-stagger-swagger) [![Ember Observer Score](http://emberobserver.com/badges/ember-stagger-swagger.svg)](http://emberobserver.com/addons/ember-stagger-swagger) 4 | 5 | *GPU stagger animation for Ember list items* 6 | 7 | ![](http://33.media.tumblr.com/29addca2c908d96a071932761ffd177a/tumblr_nstg1jgKcg1uruo10o1_500.gif) 8 | 9 | See the demo [here][demo]. 10 | 11 | 12 | ## Installation 13 | ``` 14 | ember install ember-stagger-swagger 15 | ``` 16 | 17 | ## Usage 18 | 19 | `ember-stagger-swagger` ships with a `stagger-set` component that can be used directly in a template to wrap the items that you wish to animate. 20 | 21 | Conceptually, the component treats all direct child elements as its _set items_ or _list items_: 22 | 23 | ```htmlbars 24 | 25 |

    Spell Ingredients

    26 | 27 | {{#stagger-set inDirection="right" inEffect="slideAndFade"}} 28 | 29 | {{#each potions as |potion|}} 30 |
  • {{potion.name}}
  • 31 | {{/each}} 32 | 33 | {{/stagger-set}} 34 | 35 | ``` 36 | 37 | Additionally, `ember-stagger-swagger` exposes a mixin that can be imported directly from the addon and extended however you see fit: 38 | ```js.es6 39 | import StaggerSetMixin from 'ember-stagger-swagger/mixins/stagger-set'; 40 | ``` 41 | 42 | ## API 43 | 44 | ##### `inDirection` 45 | * _description_: The direction of animation when the items stagger into view. 46 | * ***_required_: yes*** 47 | * _constraints_: A string keyword matching either `'left'`, `'down'`, `'right'`, or `'up'`. 48 | 49 | ##### `outDirection` 50 | * _description_: The direction of animation when the items stagger out of view. 51 | * _required_: no 52 | * _default_: The provided `inDirection`. 53 | * _constraints_: A string keyword matching either `'left'`, `'down'`, `'right'`, or `'up'`. 54 | 55 | ##### `inEffect` 56 | * _description_: A recognized animation effect applied to each list item when it's animating in. 57 | * ***_required_: yes*** 58 | * _constraints_: A string keyword matching either `'slideAndFade'`, `'slide'`, `'fade'`, or `'scale'` (see the [demo][demo] for a preview of each). 59 | 60 | ##### `outEffect` 61 | * _description_: A recognized animation effect applied to each list item when it's animating in. 62 | * _required_: no 63 | * _default_: the provided `inEffect` 64 | * _constraints_: A string keyword matching either `'slideAndFade'`, `'slide'`, `'fade'`, or `'scale'` (see the [demo][demo] for a preview of each). 65 | 66 | ##### `customInEffect` 67 | * _description_: An [animation name](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-name) matching a [CSS animation keyframe](https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes) that you have defined in your project. If specified alongside an `inEffect`, this name will take precedence. This can be used to create your own effects and integrate them with `stagger-swagger`'s built-in functionality. 68 | * _required_: no 69 | * _default_: `null` 70 | * _constraints_: None. Just make sure the name matches a CSS animation keyframe that you have defined in your project. 71 | 72 | ##### `customOutEffect` 73 | * _description_: An [animation name](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-name) matching a [CSS animation keyframe](https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes) that you have defined in your project. If specified alongside an `outEffect`, this name will take precedence. This can be used to create your own effects and integrate them with `stagger-swagger`'s built-in functionality. 74 | * _required_: no 75 | * _default_: `null` 76 | * _constraints_: None. Just make sure the name matches a CSS animation keyframe that you have defined in your project. 77 | 78 | 79 | ##### `showItems` 80 | * _description_: Manual hook for toggling the set between its entrance and exit animations. 81 | * _required_: no 82 | * _default_: `true` to correspond with [`enterOnRender`](#api-enterOnRender)'s default. 83 | * _constraints_: `true` or `false` 84 | 85 | 86 | ##### `enterOnRender` 87 | * _description_: Whether or not the elements in the stagger set should animate into view when the component is rendered. 88 | *Note:* `enterOnRender` allows for a 89 | more fine-grained level of control than just using `showItems`. Without `enterOnRender`, initializing the component with `showItems` set to `true` will cause the items to render in their normal visible state, from which the animation can be toggled further. Setting `enterOnRender` to true -- in conjunction with setting `showItems` to true (both of which are the default) -- creates a stagger-in animation on render and then hinges on the state of `showItems` going forward. 90 | * _required_: no 91 | * _default_: `true` 92 | * _constraints_: `true` or `false` 93 | 94 | 95 | ##### `staggerInterval` 96 | * _description_: the number of milliseconds between the animation of successive items in the set. 97 | * _required_: no 98 | * _default_: 32ms. 2 frames per item (1 frame @ 60fps ~= 16ms) creates a noticeably staggered but still-perceptively [smooth and fluid motion](https://en.wikipedia.org/wiki/Traditional_animation#.22Shooting_on_twos.22). 99 | * _constraints_: a number value greater than or equal to 32. 100 | 101 | 102 | ##### `inDelay` 103 | * _description_: Duration of delay for the items' entrance animation (when the animation is activated). 104 | * _required_: no 105 | * _default_: 0 106 | * _constraints_: a number value greater than or equal to 0. 107 | 108 | ##### `outDelay` 109 | * _description_: Duration of delay for the items' exit animation (when the animation is activated). 110 | * _required_: no 111 | * _default_: 0 112 | * _constraints_: a number value greater than or equal to 0. 113 | 114 | 115 | ##### `inTimingFunc` 116 | * _description_: The [animation-timing-function](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function) applied to each item in the set when it's animating in. 117 | * _required_: no 118 | * _default_: `cubic-bezier(0.215, 0.610, 0.355, 1.000)` (AKA ["ease-out-cubic"](http://easings.net/#easeOutCubic)) 119 | * _constraints_: a string matching any [valid CSS `timing-function` value](https://developer.mozilla.org/en-US/docs/Web/CSS/timing-function). If this value is invalid, the browser will default to using [`ease`](http://cubic-bezier.com/#.25,.1,.25,1). 120 | 121 | 122 | ##### `outTimingFunc` 123 | * _description_: The [animation-timing-function](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function) applied to each item in the set when it's animating out. 124 | * _required_: no 125 | * _default_: `cubic-bezier(0.55, 0.055, 0.675, 0.19)` (AKA ["ease-in-cubic"](http://easings.net/#easeInCubic)) 126 | * _constraints_: a string matching any [valid CSS `timing-function` value](https://developer.mozilla.org/en-US/docs/Web/CSS/timing-function). If this value is invalid, the browser will default to using [`ease`](http://cubic-bezier.com/#.25,.1,.25,1). 127 | 128 | ##### `inDuration` 129 | * _description_: The duration (in milliseconds) that *a single item* will take when animating in. 130 | * _required_: no 131 | * _default_: 500 132 | * _constraints_: a numeric value greater than 0 133 | 134 | ##### `outDuration` 135 | * _description_: The duration (in milliseconds) that *a single item* will take when animating out. 136 | * _required_: no 137 | * _default_: 500 138 | * _constraints_: a numeric value greater than 0 139 | 140 | ##### `duration` 141 | * _description_: A convenience property to set a single duration on both the entrance and exit animations. If set alongside any `inDuration` or `outDuration`, this property will take precedence 142 | * _required_: no 143 | * _default_: null (property is ignored if unset), 144 | * _constraints_: a numeric value greater than zero 145 | 146 | 147 | 148 | 149 | ### Practical Tips 150 | 151 | #### Styling 152 | Because the DOM elements of Ember components are, by default, `
    `s, and because it handles setting an `animationName` property on the component's direct children, you can safely design, conceptualize, and style your child elements as you normally would for the list items of a relative container. 153 | 154 | Furthermore, because the keyframes for the built-in effects of `slide` and `slideAndFade` define transforms to bring their element in or out of its container's visible bounds (e.g., `transform: translate3d(-100, 0, 0)` at the 100%-stop of a left slide), it may well be useful to restrict overflow on the top-level component's element so that the children disappear when they're outside of said bounds. 155 | 156 | The [stagger-set "list items" demo][demo] is an example of how this would appear. 157 | 158 | #### Creating Animation Effects 159 | By default, a `stagger-list` component will attempt to map the keywords provided for `inEffect` or `outEffect` to one of its [built-in keyframes](/app/styles/_keyframes.css). 160 | 161 | However, you're free to implement your own keyframes and have them called instead. Just define them in your stylesheets as you would normally, and then pass the keyframe name to a `stagger-list` as a string argument for either `customInEffect` or `customOutEffect`. When these attributes are defined, `stagger-list` will always set them on the `animation-name` property of its child elements' style definition at the appropriate time. 162 | 163 | ## Action Hooks 164 | 165 | * `onAnimationStart`: called immediately after the _first_ item in the set triggers its [`animationstart`](https://developer.mozilla.org/en-US/docs/Web/Events/animationstart) event. 166 | * called with: 167 | * `animationEvent`: the [`animationevent` object](https://developer.mozilla.org/en-US/docs/Web/Events/animationstart#Properties) 168 | 169 | * `onAnimationComplete`: called immediately after the _last_ item in the set triggers its [`animationend`](https://developer.mozilla.org/en-US/docs/Web/Events/animationend) event. 170 | * called with: 171 | * `animationEvent`: the [`animationevent` object](https://developer.mozilla.org/en-US/docs/Web/Events/animationend#Properties) 172 | 173 | Together, these hooks can provide more control over interactions with the component during its animation. For example, if you set up a button to trigger toggles of the animation, you might want to make sure that it's disabled between the start and completion events. 174 | 175 | ```htmlbars 176 | 177 |

    Spell Ingredients

    178 | 179 | {{#stagger-set 180 | inDirection="right" 181 | inEffect="slideAndFade" 182 | onAnimationStart=(action 'loadCannons') 183 | onAnimationComplete=(action 'fireCannons') 184 | }} 185 | 186 | {{#each potions as |potion|}} 187 |
  • {{potion.name}}
  • 188 | {{/each}} 189 | 190 | {{/stagger-set}} 191 | 192 | ``` 193 | 194 | 195 | ## Future Goals & Ideas 196 | * _Tentative_: [Explore improved effects for the vertical `slide` animation](#link-to-issue)? 197 | * possibly break the mixins apart to deal with vertical and horizontal animation separately? 198 | * _Tentative_: Removing need for any CSS by using the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API)? 199 | * Libraries like GSAP or Velocity are great for enabling pure JavaScript animation today (see: [`liquid-fire-velocity`](https://github.com/ember-animation/liquid-fire-velocity)), but they're a bit too heavy for this addon, which only seeks to provide a handful of base effects and mainly concerns itself with encapsulating functionality. 200 | 201 | 202 | ## Developing Locally 203 | 204 | ### Installation 205 | 206 | * `git clone` this repository 207 | * `npm install` 208 | * `bower install` 209 | 210 | ### Running 211 | 212 | * `ember server` 213 | * Visit your app at http://localhost:4200. 214 | 215 | ### Running Tests 216 | 217 | * `npm test` (Runs `ember try:testall` to test your addon against multiple Ember versions) 218 | * `ember test` 219 | * `ember test --server` 220 | 221 | ### Building 222 | 223 | * `ember build` 224 | 225 | For more information on using ember-cli, visit [http://www.ember-cli.com/](http://www.ember-cli.com/). 226 | 227 | 228 | 229 | [demo]: http://www.sipple.io/ember-stagger-swagger-demo/ 230 | -------------------------------------------------------------------------------- /addon/mixins/stagger-set.js: -------------------------------------------------------------------------------- 1 | /** 2 | * stagger-set mixin 3 | * 4 | * A component mixin that renders list items by performing a staggered animation of their 5 | * entrance/exit by listening to a "showItems" binding. 6 | * 7 | * A "list item" is considered anything that's supplied as a direct child 8 | * element of the component's template block. 9 | */ 10 | import Ember from 'ember'; 11 | import Constants from 'ember-stagger-swagger/constants/constants'; 12 | import getAnimationPrefix from 'ember-stagger-swagger/utils/get-animation-prefix'; 13 | 14 | const { 15 | Mixin, 16 | computed, 17 | run, 18 | Logger: { warn }, 19 | assert, 20 | isNone, 21 | } = Ember; 22 | 23 | const { 24 | notEmpty, 25 | } = computed; 26 | 27 | const { 28 | CLASS_NAMES, 29 | DEFAULTS, 30 | ANIMATION_DIRECTIONS, 31 | ANIMATION_NAMES, 32 | KEYFRAMES_MAP, 33 | } = Constants; 34 | 35 | const warningPreface = 'ember-stagger-swagger:stagger-set-mixin: '; 36 | 37 | export default Mixin.create({ 38 | 39 | classNames: ['__ember-stagger-swagger__stagger-set'], 40 | classNameBindings: [`hasListToggled::${CLASS_NAMES.untoggled}`], 41 | 42 | /* -------------------------------------------------------------------- * 43 | * ----------------------- API ------------------------ * 44 | * -------------------------------------------------------------------- */ 45 | inDirection: null, 46 | outDirection: null, 47 | inEffect: null, 48 | outEffect: null, 49 | customInEffect: null, 50 | customOutEffect: null, 51 | 52 | /* trigger the entrance animation when this element is inserted into the DOM */ 53 | enterOnRender: true, 54 | 55 | /** 56 | * Flag for manually triggering either the show or hide animation 57 | * 58 | * By default, the list items will animate in on render, but 59 | * but this allows the user to have full toggle control if they want it. 60 | */ 61 | showItems: true, 62 | 63 | /* MILLESECONDS */ 64 | staggerInterval: null, 65 | 66 | /* MILLESECONDS */ 67 | inDelay: null, 68 | outDelay: null, 69 | 70 | /* Timing Function */ 71 | inTimingFunc: null, 72 | outTimingFunc: null, 73 | timingFunc: null, // single timing function for both in and out 74 | 75 | /* Duration (milliseconds) */ 76 | inDuration: null, 77 | outDuration: null, 78 | duration: null, // convinience option for a single in/out duration 79 | /* -------------------------------------------------------------------- * 80 | * ----------------------- /API ------------------------ * 81 | * -------------------------------------------------------------------- */ 82 | 83 | 84 | isAnimating: false, 85 | hasListToggled: false, 86 | 87 | /** 88 | * Callback (to be initialized) for our animationstart event listener 89 | */ 90 | _onStaggerStart: null, 91 | 92 | /** 93 | * Callback (to be initialized) for our animationend event listener 94 | */ 95 | _onStaggerComplete: null, 96 | 97 | /** 98 | * Callback (to be initialized) for caching the DOM nodes of our child elements 99 | * when they get added to the DOM 100 | */ 101 | _onChildElementsInserted: null, 102 | _childInsertionListener: null, 103 | 104 | /** 105 | * Array of cached "list item" elements to cache upon insertion 106 | */ 107 | _listItemElems: null, 108 | 109 | /** 110 | * We'll be caching the animation prefix (if any) that's needed to 111 | * modify DOM element style properties. 112 | */ 113 | _animationPrefix: null, 114 | 115 | hasItemsToAnimate: notEmpty('_listItemElems'), 116 | 117 | isReadyToAnimate: computed('hasItemsToAnimate', 'isAnimating', function isReadyToAnimate () { 118 | return this.get('hasItemsToAnimate') && !this.get('isAnimating'); 119 | }), 120 | 121 | _inAnimationName: computed('inEffect', 'customInEffect', 'inDirection', function computeInAnimationName () { 122 | return this.get('customInEffect') || KEYFRAMES_MAP[this.get('inDirection')].in[this.get('inEffect')]; 123 | }), 124 | 125 | _outAnimationName: computed('outEffect', 'customOutEffect', 'outDirection', function computeOutAnimationName () { 126 | return this.get('customOutEffect') || KEYFRAMES_MAP[this.get('outDirection')].out[this.get('outEffect')]; 127 | }), 128 | 129 | needsToEnterAfterInit: computed('enterOnRender', 'hasListToggled', 'isReadyToAnimate', 'showItems', function needsInitialRender () { 130 | return ( 131 | !this.get('hasListToggled') && 132 | this.get('enterOnRender') && 133 | this.get('showItems') && 134 | this.get('isReadyToAnimate') 135 | ); 136 | }), 137 | 138 | currentAnimationName: computed( 139 | '_inAnimationName', 140 | '_outAnimationName', 141 | 'showItems', 142 | 'needsToEnterAfterInit', 143 | function currentAnimationName() { 144 | return ( this.get('showItems') || this.get('needsToEnterAfterInit') ) ? 145 | this.get('_inAnimationName') 146 | : 147 | this.get('_outAnimationName'); 148 | } 149 | ), 150 | 151 | currentAnimationDuration: computed('duration', 'inDuration', 'outDuration', 'showItems', function computeDuration () { 152 | // give priority to a valid general duration 153 | const generalDuration = this.get('duration'); 154 | if (!isNone(generalDuration) && !Number.isNaN(Number(generalDuration)) && generalDuration > 0) { 155 | return generalDuration; 156 | } 157 | 158 | // otherwise, set according to the state of showItems 159 | return this.get('showItems') ? this.get('inDuration') : this.get('outDuration'); 160 | }), 161 | 162 | currentAnimationTimingFunction: computed('inTimingFunc', 'outTimingFunc', 'showItems', function computeCurrentTimingFunction () { 163 | // otherwise, set according to the state of showItems 164 | return this.get('showItems') ? this.get('inTimingFunc') : this.get('outTimingFunc'); 165 | }), 166 | 167 | /* -------------------- LIFECYCLE HOOKS ---------------------- */ 168 | init () { 169 | this._super(...arguments); 170 | 171 | this._resolveInitialTimingAttrs(); 172 | this._resolveInitialEffectAttrs(); 173 | }, 174 | 175 | 176 | didInsertElement () { 177 | this._super(...arguments); 178 | 179 | this._initAnimationCallbacks(); 180 | 181 | if (this.element.children && this.element.children.length) { 182 | this._syncWithDOM(); 183 | 184 | } else { 185 | this._initChildInsertionCallback(); 186 | this._initChildInsertionListener(); 187 | } 188 | }, 189 | 190 | 191 | /** 192 | * Trigger the staggering animation when something on the outside updates `showItems` 193 | */ 194 | didUpdateAttrs (attrData) { 195 | this._super(...arguments); 196 | 197 | const oldShowItems = ( 198 | typeof attrData.oldAttrs.showItems.value !== 'undefined' && 199 | attrData.oldAttrs.showItems.value 200 | ); 201 | 202 | const newShowItems = ( 203 | typeof attrData.newAttrs.showItems.value !== 'undefined' && 204 | attrData.newAttrs.showItems.value 205 | ); 206 | 207 | if (oldShowItems !== newShowItems && this.get('isReadyToAnimate')) { 208 | run.scheduleOnce('afterRender', this, '_triggerAnimation'); 209 | } 210 | }, 211 | 212 | 213 | willDestroyElement () { 214 | this._super(...arguments); 215 | 216 | const animationPrefix = this.get('animationPrefix'); 217 | const startEvent = animationPrefix ? `${animationPrefix}AnimationStart` : 'animationstart'; 218 | const endEvent = animationPrefix ? `${animationPrefix}AnimationEnd` : 'animationend'; 219 | 220 | this.element.removeEventListener(`${startEvent}`, this._onStaggerStart, false); 221 | this.element.removeEventListener(`${endEvent}`, this._onStaggerComplete, false); 222 | 223 | 224 | this.set('_onStaggerStart', null); 225 | this.set('_onStaggerComplete', null); 226 | this.set('_listItemElems', null); 227 | 228 | if (this._childInsertionListener) { 229 | this._childInsertionListener.disconnect(); 230 | this.set('_childInsertionListener', null); 231 | this.set('_onChildElementsInserted', null); 232 | } 233 | }, 234 | 235 | /* -------------------- ACTIONS ---------------------- */ 236 | actions: { 237 | 238 | broadcastAnimationStart(animationEvent) { 239 | if (typeof this.onAnimationStart === 'function') { 240 | this.onAnimationStart(animationEvent); 241 | } 242 | }, 243 | 244 | broadcastAnimationComplete(animationEvent) { 245 | if (typeof this.onAnimationComplete === 'function') { 246 | this.onAnimationComplete(animationEvent); 247 | } 248 | }, 249 | }, 250 | 251 | /* -------------------- HELPERS ---------------------- */ 252 | _cacheListItems () { 253 | this.set('_listItemElems', Array.from(this.element.children)); 254 | }, 255 | 256 | _cacheAnimationPrefix () { 257 | this.set('_animationPrefix', getAnimationPrefix(this.element)); 258 | }, 259 | 260 | _initAnimationCallbacks () { 261 | 262 | this._onStaggerStart = function onStaggerStart (event) { 263 | this.set('isAnimating', true); 264 | this.send('broadcastAnimationStart', event); 265 | 266 | }.bind(this); 267 | 268 | /** 269 | * AnimationEvent listener for the `animationend` event fired by each 270 | * child item. 271 | * 272 | * If the event corresponds to the last item, we'll trigger a run loop call 273 | * that will fire immediately after the animation and 274 | * update `hasListToggled` (if necessary), `isAnimating`, and 275 | * tell our `broadcastAnimationComplete` action to fire 276 | */ 277 | this._onStaggerComplete = function onStaggerComplete (event) { 278 | // only update the DOM after we've finished animating all items 279 | const hasListToggled = this.get('hasListToggled'); 280 | const lastListItemElem = this._listItemElems[this._listItemElems.length - 1]; 281 | const targetElem = event.target; 282 | 283 | if (!hasListToggled) { 284 | targetElem.classList.add(CLASS_NAMES.listItemCompletedInitialToggle); 285 | } 286 | 287 | if (Object.is(targetElem, lastListItemElem)) { 288 | run.once(() => { 289 | if (!hasListToggled) { 290 | this.set('hasListToggled', true); 291 | } 292 | this.set('isAnimating', false); 293 | this.send('broadcastAnimationComplete', event); 294 | }); 295 | } 296 | 297 | }.bind(this); 298 | }, 299 | 300 | _syncWithDOM () { 301 | this._cacheListItems(); 302 | this._cacheAnimationPrefix(); 303 | this._prepareItemsInDOM(); 304 | if (this.get('needsToEnterAfterInit')) { 305 | run.scheduleOnce('afterRender', this, '_triggerAnimation'); 306 | } 307 | }, 308 | 309 | _initChildInsertionCallback () { 310 | this._onChildElementsInserted = function onChildElementsInserted () { 311 | run.scheduleOnce('afterRender', this, () => { 312 | this._syncWithDOM(); 313 | }); 314 | } 315 | }, 316 | 317 | /** 318 | * For components whose child elements are asynchronously rendered, 319 | * we can listen for the completion of such rendering and cache our 320 | * list items then. 321 | */ 322 | _initChildInsertionListener () { 323 | this._childInsertionListener = new MutationObserver(function childInsertionListener (mutations) { 324 | this._onChildElementsInserted(); 325 | }.bind(this)); 326 | 327 | this._childInsertionListener.observe(this.element, { childList: true }); 328 | }, 329 | 330 | _prepareItemsInDOM () { 331 | this._setAnimationValuesForItems(false); 332 | 333 | const animationPrefix = this.get('_animationPrefix'); 334 | const startEvent = animationPrefix ? `${animationPrefix}AnimationStart` : 'animationstart'; 335 | const endEvent = animationPrefix ? `${animationPrefix}AnimationEnd` : 'animationend'; 336 | 337 | this.element.addEventListener(`${startEvent}`, this._onStaggerStart, false); 338 | this.element.addEventListener(`${endEvent}`, this._onStaggerComplete, false); 339 | }, 340 | 341 | 342 | /** 343 | * helper function called on init that ensures we're wired up 344 | * with acceptable timing properties, PROVIDING WARNINGS when 345 | * the user specified something invalid 346 | */ 347 | _resolveInitialTimingAttrs () { 348 | 349 | this.staggerInterval = this.staggerInterval || DEFAULTS.STAGGER_INTERVAL_MS; 350 | this.inDelay = this.inDelay || DEFAULTS.ANIMATION_DELAY_IN; 351 | this.outDelay = this.outDelay || DEFAULTS.ANIMATION_DELAY_OUT; 352 | this.inDuration = this.inDuration || DEFAULTS.ANIMATION_DURATION_IN; 353 | this.outDuration = this.outDuration || DEFAULTS.ANIMATION_DURATION_OUT; 354 | 355 | /* eslint-disable max-len */ 356 | if (isNone(this.staggerInterval) || Number.isNaN(Number(this.staggerInterval)) || this.staggerInterval < 32) { 357 | warn(warningPreface, `Invalid \`staggerInterval\`: ${this.staggerInterval}. Please use a numeric value greater than or equal to 32 (milliseconds). Defaulting to ${DEFAULTS.STAGGER_INTERVAL_MS} milliseconds`); 358 | this.staggerInterval = DEFAULTS.STAGGER_INTERVAL_MS; 359 | } 360 | 361 | if (isNone(this.inDelay) || Number.isNaN(Number(this.inDelay)) || this.inDelay < 0) { 362 | warn(warningPreface, `Invalid \`inDelay\`: ${this.inDelay}. Please use a numeric value greater than or equal to zero. Defaulting to ${DEFAULTS.ANIMATION_DELAY_IN}`); 363 | this.inDelay = DEFAULTS.ANIMATION_DELAY_IN; 364 | } 365 | 366 | if (isNone(this.outDelay) || Number.isNaN(Number(this.outDelay)) || this.outDelay < 0) { 367 | warn(warningPreface, `Invalid \`outDelay\`: ${this.outDelay}. Please use a numeric value greater than or equal to zero. Defaulting to ${DEFAULTS.ANIMATION_DELAY_OUT}`); 368 | this.outDelay = DEFAULTS.ANIMATION_DELAY_IN; 369 | } 370 | 371 | if (isNone(this.inDuration) || Number.isNaN(Number(this.inDuration)) || this.inDuration <= 0) { 372 | warn(warningPreface, `Invalid \`inDuration\`: ${this.inDuration}. Please use a numeric value greater than zero. Defaulting to ${DEFAULTS.ANIMATION_DURATION_IN}`); 373 | this.inDuration = DEFAULTS.ANIMATION_DURATION_IN; 374 | } 375 | 376 | if (isNone(this.outDuration) || Number.isNaN(Number(this.outDuration)) || this.outDuration <= 0) { 377 | warn(warningPreface, `Invalid \`outDuration\`: ${this.outDuration}. Please use a numeric value greater than zero. Defaulting to ${DEFAULTS.ANIMATION_DURATION_OUT}`); 378 | this.outDuration = DEFAULTS.ANIMATION_DURATION_OUT; 379 | } 380 | /* eslint-enable max-len */ 381 | 382 | // Set a default timing functoin if none is provided, but don't warn 383 | if (isNone(this.inTimingFunc)) { 384 | this.inTimingFunc = DEFAULTS.TIMING_FUNCTION_IN; 385 | } 386 | if (isNone(this.outTimingFunc)) { 387 | this.outTimingFunc = DEFAULTS.TIMING_FUNCTION_OUT; 388 | } 389 | }, 390 | 391 | /** 392 | * helper function called on init that ensures we're wired up 393 | * with acceptable aniamation effect properties, THROWING ERRORS when 394 | * required attributes are invalid and warning when optional attributes 395 | * are set but invalid. 396 | */ 397 | _resolveInitialEffectAttrs () { 398 | assert( 399 | `stagger-set must have a valid \`inDirection\`. Received \`${this.inDirection}\``, // TODO: Link to docs 400 | !isNone(this.inDirection) && !!KEYFRAMES_MAP[this.inDirection] 401 | ); 402 | assert( 403 | `stagger-set must have a valid \`inEffect\`. Received \`${this.inEffect}\``, // TODO: Link to docs 404 | !isNone(this.inEffect) && !!KEYFRAMES_MAP[this.inDirection].in[this.inEffect] 405 | ); 406 | 407 | /* eslint-disable max-len */ 408 | if (isNone(this.outDirection)) { 409 | this.outDirection = this.inDirection; 410 | 411 | } else if (!KEYFRAMES_MAP[this.outDirection]) { 412 | warn(warningPreface, `Invalid \`outDirection\`: ${this.outDirection}. Defaulting to ${this.inDirection}`); 413 | this.outDirection = this.inDirection; 414 | } 415 | 416 | if (isNone(this.outEffect)) { 417 | this.outEffect = this.inEffect; 418 | 419 | } else if (!KEYFRAMES_MAP[this.outDirection].out[this.outEffect]) { 420 | warn(warningPreface, `Invalid \`outEffect\`: ${this.outEffect}. Defaulting to ${this.inEffect}`); 421 | this.outEffect = this.inEffect; 422 | } 423 | /* eslint-enable max-len */ 424 | }, 425 | 426 | _setAnimationValuesForItems (applyName = false) { 427 | 428 | // setting an animation name is what we use to trigger the animation, so only set if asked for 429 | const currentAnimationName = applyName ? this.get('currentAnimationName'): ''; 430 | const currentStaggerInterval = this.get('staggerInterval'); 431 | const currentAnimationDuration = `${this.get('currentAnimationDuration')}ms`; 432 | const currentAnimationTimingFunction = this.get('currentAnimationTimingFunction'); 433 | 434 | // inDelay / outDelay 435 | const currentDelay = this.get('showItems') ? this.get('inDelay') : this.get('outDelay'); 436 | 437 | const animationPrefix = this.get('_animationPrefix'); 438 | const propertyPrefix = animationPrefix ? `${animationPrefix}A` : 'a'; 439 | 440 | this._listItemElems.forEach((listItemElem, idx) => { 441 | listItemElem.style[`${propertyPrefix}nimationDelay`] = `${(currentStaggerInterval * (idx + 1)) + currentDelay}ms`; 442 | listItemElem.style[`${propertyPrefix}nimationTimingFunction`] = currentAnimationTimingFunction; 443 | listItemElem.style[`${propertyPrefix}nimationDuration`] = currentAnimationDuration; 444 | listItemElem.style[`${propertyPrefix}nimationName`] = currentAnimationName; 445 | listItemElem.style[`${propertyPrefix}nimationFillMode`] = 'both'; 446 | }); 447 | }, 448 | 449 | _triggerAnimation() { 450 | this._setAnimationValuesForItems(true); 451 | }, 452 | 453 | }); 454 | -------------------------------------------------------------------------------- /tests/dummy/public/savvy.min.css: -------------------------------------------------------------------------------- 1 | html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}iframe{border:0}h1,h2,h3,h4,h5,p,blockquote,figure,button{margin:0;padding:0}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}small{font-size:80%}sub,sup{font-size:65%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}svg:not(:root){overflow:hidden}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible;font-size:inherit;background:none;-webkit-font-smoothing:inherit;letter-spacing:inherit;cursor:pointer;color:currentColor}button:focus{outline:0}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button:-moz-focusring,input:-moz-focusring{outline:1px dotted ButtonText}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}img,object,embed,video{max-width:100%;height:auto;border:0}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}html{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Avenir,Geneva,Tahoma,san-serif;color:rgba(0,0,0,.87)}input{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Avenir,Geneva,Tahoma,san-serif;color:rgba(0,0,0,.87)}select{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Avenir,Geneva,Tahoma,san-serif;color:rgba(0,0,0,.87)}textarea{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Avenir,Geneva,Tahoma,san-serif;color:rgba(0,0,0,.87)}address{font-style:normal}html{font-size:100%;-webkit-font-kerning:normal;font-kerning:normal;-webkit-font-variant-ligatures:common-ligatures,contextual;font-variant-ligatures:common-ligatures,contextual;-ms-font-feature-settings:"kern","liga","clig","alt";-webkit-font-feature-settings:"kern","liga","clig","calt";font-feature-settings:"kern","liga","clig","calt","kern",common-ligatures,contextual}a{text-decoration:none;color:currentColor;background-color:transparent;background-image:-webkit-linear-gradient(transparent 50%,currentColor 50%);background-image:linear-gradient(transparent 50%,currentColor 50%);background-repeat:repeat-x;background-size:2px 2px;background-position:0 90%;-webkit-transition:all .36s;transition:all .36s;-webkit-transition-timing-function:ease;transition-timing-function:ease}a:visited{color:currentColor}a:focus{color:#00a7f5}a:hover{color:#00a7f5}a:focus{color:#00a7f5}a:active{color:#48c1f9}a:active,a:hover,a:focus{outline:0}blockquote{font-size:1.2em;line-height:1.2em}body{font-size:.83333em;line-height:1.44em}h1,h2,h3,h4,h5,h6{text-rendering:optimizeLegibility}h1{font-size:2.0736em;letter-spacing:-.06em}h2{font-size:1.728em;letter-spacing:-.04em}h3{font-size:1.44em;letter-spacing:-.02em}h4{font-size:1.2em}h5{font-size:1em}h6{font-size:.83333em}h1{line-height:1em}h2{line-height:1.2em}h3{line-height:1.2em}h4{line-height:1.2em}h5{line-height:1.2em}h6{line-height:1.2em}dt{font-weight:700}dd{margin-left:1.25em}ul{padding-left:1.25em}ol{padding-left:1.25em}p{margin:0}p+p{margin-top:1.5em}.o-flex-grid{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;list-style:none;margin:0;padding:0}.o-flex-grid .o-grid-cell{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;-webkit-flex-shrink:1;-ms-flex-negative:1;flex-shrink:1;-webkit-flex-basis:0%;-ms-flex-preferred-size:0;flex-basis:0%;box-sizing:border-box}.o-flex-grid>.o-grid-cell--grow-none{-webkit-box-flex:0;-webkit-flex:0 0 0%;-ms-flex:0 0 0%;flex:0 0 0%}.o-flex-grid>.o-grid-cell--grow-one{-webkit-box-flex:1;-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%}.o-flex-grid>.o-grid-cell--auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto}.o-flex-grid.o-flex-grid--noWrap{-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap}.o-flex-grid.o-flex-grid--flex-cells>.o-grid-cell{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.o-flex-grid.o-flex-grid--top{-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.o-flex-grid.o-flex-grid--bottom{-webkit-box-align:end;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end}.o-flex-grid.o-flex-grid--center{-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.o-flex-grid.o-flex-grid--justifyStart{-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}.o-flex-grid.o-flex-grid--justifyCenter{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.o-flex-grid.o-flex-grid--justifyEnd{-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}.o-flex-grid.o-flex-grid--spaceAround{-webkit-justify-content:space-around;-ms-flex-pack:distribute;justify-content:space-around}.o-flex-grid.o-flex-grid--spaceBetween{-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.o-flex-grid .o-grid-cell--top{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start}.o-flex-grid .o-grid-cell--bottom{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end}.o-flex-grid .o-grid-cell--center{-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.o-flex-grid .o-grid-cell--autoSize{-webkit-box-flex:0;-webkit-flex:none;-ms-flex:none;flex:none}.o-flex-grid.o-flex-grid--fit>.o-grid-cell{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.o-flex-grid.o-flex-grid--full>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.o-flex-grid.o-flex-grid--auto>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto}.o-flex-grid.o-flex-grid--oneHalf>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%}.o-flex-grid.o-flex-grid--oneThird>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 33.33333%;-ms-flex:0 0 33.33333%;flex:0 0 33.33333%}.o-flex-grid.o-flex-grid--oneQuarter>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%}.o-flex-grid.o-flex-grid--gutters{margin:-1em 0 0 -1em}.o-flex-grid.o-flex-grid--gutters>.o-grid-cell{padding:1em 0 0 1em}.o-flex-grid.o-flex-grid--guttersLg{margin:-1.5em 0 0 -1.5em}.o-flex-grid.o-flex-grid--guttersLg>.o-grid-cell{padding:1.5em 0 0 1.5em}.o-flex-grid.o-flex-grid--guttersXl{margin:-2em 0 0 -2em}.o-flex-grid.o-flex-grid--guttersXl>.o-grid-cell{padding:2em 0 0 2em}.o-divider-1--horizontal{height:1px}.o-divider-2--horizontal{height:2px}.o-divider-1--vertical{width:1px}.o-divider-2--vertical{width:2px}.o-link--no-under{text-decoration:none;background:none}.o-scroll-container--vertical{overflow-y:scroll}.o-scroll-container--horizontal{overflow-x:scroll}.o-clip-container{overflow:hidden}.o-list--flat,.o-list--reset{list-style-type:none;margin-left:0;padding-left:0}.a-anim-ease-default{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1.000);animation-timing-function:cubic-bezier(.215,.61,.355,1.000)}.a-trans-ease-default{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1.000);transition-timing-function:cubic-bezier(.215,.61,.355,1.000)}.a-anim-ease-scaleUp{-webkit-animation-timing-function:cubic-bezier(.23,1.000,.32,1.000);animation-timing-function:cubic-bezier(.23,1.000,.32,1.000)}.a-trans-ease-scaleUp{-webkit-transition-timing-function:cubic-bezier(.23,1.000,.32,1.000);transition-timing-function:cubic-bezier(.23,1.000,.32,1.000)}.a-anim-ease-scaleUp--bounce{-webkit-animation-timing-function:cubic-bezier(0.00,.3,0.00,1.48);animation-timing-function:cubic-bezier(0.00,.3,0.00,1.48)}.a-trans-ease-scaleUp--bounce{-webkit-transition-timing-function:cubic-bezier(0.00,.3,0.00,1.48);transition-timing-function:cubic-bezier(0.00,.3,0.00,1.48)}.a-anim-ease-scaleDown{-webkit-animation-timing-function:cubic-bezier(.47,0.000,.745,.715);animation-timing-function:cubic-bezier(.47,0.000,.745,.715)}.a-trans-ease-scaleDown{-webkit-transition-timing-function:cubic-bezier(.47,0.000,.745,.715);transition-timing-function:cubic-bezier(.47,0.000,.745,.715)}.a-anim-ease-fadeIn{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1.000);animation-timing-function:cubic-bezier(.215,.61,.355,1.000)}.a-trans-ease-fadeIn{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1.000);transition-timing-function:cubic-bezier(.215,.61,.355,1.000)}.a-anim-ease-fadeIn--bounce{-webkit-animation-timing-function:cubic-bezier(0.00,.3,0.00,1.48);animation-timing-function:cubic-bezier(0.00,.3,0.00,1.48)}.a-trans-ease-fadeIn--bounce{-webkit-transition-timing-function:cubic-bezier(0.00,.3,0.00,1.48);transition-timing-function:cubic-bezier(0.00,.3,0.00,1.48)}.a-anim-ease-fadeOut{-webkit-animation-timing-function:cubic-bezier(.47,0.000,.745,.715);animation-timing-function:cubic-bezier(.47,0.000,.745,.715)}.a-trans-ease-fadeOut{-webkit-transition-timing-function:cubic-bezier(.47,0.000,.745,.715);transition-timing-function:cubic-bezier(.47,0.000,.745,.715)}.a-anim-ease-slideIn--light{-webkit-animation-timing-function:cubic-bezier(.165,.84,.44,1.000);animation-timing-function:cubic-bezier(.165,.84,.44,1.000)}.a-trans-ease-slideIn--light{-webkit-transition-timing-function:cubic-bezier(.165,.84,.44,1.000);transition-timing-function:cubic-bezier(.165,.84,.44,1.000)}.a-anim-ease-slideIn--heavy{-webkit-animation-timing-function:cubic-bezier(.135,.535,.45,.94);animation-timing-function:cubic-bezier(.135,.535,.45,.94)}.a-trans-ease-slideIn--heavy{-webkit-transition-timing-function:cubic-bezier(.135,.535,.45,.94);transition-timing-function:cubic-bezier(.135,.535,.45,.94)}.a-anim-ease-slideOut--light{-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}.a-trans-ease-slideOut--light{-webkit-transition-timing-function:cubic-bezier(.55,.055,.675,.19);transition-timing-function:cubic-bezier(.55,.055,.675,.19)}.a-anim-ease-slideOut--heavy{-webkit-animation-timing-function:cubic-bezier(.47,.02,1.000,.57);animation-timing-function:cubic-bezier(.47,.02,1.000,.57)}.a-trans-ease-slideOut--heavy{-webkit-transition-timing-function:cubic-bezier(.47,.02,1.000,.57);transition-timing-function:cubic-bezier(.47,.02,1.000,.57)}.a-anim-ease-reposition--light{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1.000);animation-timing-function:cubic-bezier(.215,.61,.355,1.000)}.a-trans-ease-reposition--light{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1.000);transition-timing-function:cubic-bezier(.215,.61,.355,1.000)}.a-anim-ease-reposition--heavy{-webkit-animation-timing-function:cubic-bezier(.25,.46,.45,.94);animation-timing-function:cubic-bezier(.25,.46,.45,.94)}.a-trans-ease-reposition--heavy{-webkit-transition-timing-function:cubic-bezier(.25,.46,.45,.94);transition-timing-function:cubic-bezier(.25,.46,.45,.94)}.a-anim-ease-zoom{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1.000);animation-timing-function:cubic-bezier(.215,.61,.355,1.000)}.a-trans-ease-zoom{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1.000);transition-timing-function:cubic-bezier(.215,.61,.355,1.000)}.a-anim-ease-zoom--bounce{-webkit-animation-timing-function:cubic-bezier(.27,.78,.75,2.15);animation-timing-function:cubic-bezier(.27,.78,.75,2.15)}.a-trans-ease-zoom--bounce{-webkit-transition-timing-function:cubic-bezier(.27,.78,.75,2.15);transition-timing-function:cubic-bezier(.27,.78,.75,2.15)}.a-anim-ease-colorShift{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1.000);animation-timing-function:cubic-bezier(.215,.61,.355,1.000)}.a-trans-ease-colorShift{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1.000);transition-timing-function:cubic-bezier(.215,.61,.355,1.000)}.a-anim-ease-gtfo{-webkit-animation-timing-function:cubic-bezier(.68,-.63,0.00,.67);animation-timing-function:cubic-bezier(.68,-.63,0.00,.67)}.a-trans-ease-gtfo{-webkit-transition-timing-function:cubic-bezier(.68,-.63,0.00,.67);transition-timing-function:cubic-bezier(.68,-.63,0.00,.67)}.g-red--100{color:#ffccd1!important}.g-red--300{color:#e7716f!important}.g-red--500{color:#f7402b!important}.g-red--700{color:#d62c29!important}.g-red--900{color:#ba1b12!important}.g-bg-red--100{background-color:#ffccd1!important}.g-bg-red--300{background-color:#e7716f!important}.g-bg-red--500{background-color:#f7402b!important}.g-bg-red--700{background-color:#d62c29!important}.g-bg-red--900{background-color:#ba1b12!important}.g-yellow--100{color:#fffac2!important}.g-yellow--300{color:#fff36b!important}.g-yellow--500{color:#ffec1a!important}.g-yellow--700{color:#fcc203!important}.g-yellow--900{color:#f57e00!important}.g-bg-yellow--100{background-color:#fffac2!important}.g-bg-yellow--300{background-color:#fff36b!important}.g-bg-yellow--500{background-color:#ffec1a!important}.g-bg-yellow--700{background-color:#fcc203!important}.g-bg-yellow--900{background-color:#f57e00!important}.g-green--100{color:#b6f7c8!important}.g-green--300{color:#51f66c!important}.g-green--500{color:#00fa00!important}.g-green--700{color:#5abf0d!important}.g-green--900{color:#175e1c!important}.g-bg-green--100{background-color:#b6f7c8!important}.g-bg-green--300{background-color:#51f66c!important}.g-bg-green--500{background-color:#00fa00!important}.g-bg-green--700{background-color:#5abf0d!important}.g-bg-green--900{background-color:#175e1c!important}.g-aqua--100{color:#a3ffeb!important}.g-aqua--300{color:#5cffd9!important}.g-aqua--500{color:#00ebb4!important}.g-aqua--700{color:#00c2a8!important}.g-aqua--900{color:#008a7c!important}.g-bg-aqua--100{background-color:#a3ffeb!important}.g-bg-aqua--300{background-color:#5cffd9!important}.g-bg-aqua--500{background-color:#00ebb4!important}.g-bg-aqua--700{background-color:#00c2a8!important}.g-bg-aqua--900{background-color:#008a7c!important}.g-cyan--100{color:#afebf3!important}.g-cyan--300{color:#32fbf7!important}.g-cyan--500{color:#00bdd6!important}.g-cyan--700{color:#0097a8!important}.g-cyan--900{color:#006166!important}.g-bg-cyan--100{background-color:#afebf3!important}.g-bg-cyan--300{background-color:#32fbf7!important}.g-bg-cyan--500{background-color:#00bdd6!important}.g-bg-cyan--700{background-color:#0097a8!important}.g-bg-cyan--900{background-color:#006166!important}.g-blue--100{color:#afe4fd!important}.g-blue--300{color:#48c1f9!important}.g-blue--500{color:#00a7f5!important}.g-blue--700{color:#0088d6!important}.g-blue--900{color:#00549e!important}.g-bg-blue--100{background-color:#afe4fd!important}.g-bg-blue--300{background-color:#48c1f9!important}.g-bg-blue--500{background-color:#00a7f5!important}.g-bg-blue--700{background-color:#0088d6!important}.g-bg-blue--900{background-color:#00549e!important}.g-purple--100{color:#cac0f7!important}.g-purple--300{color:#8470db!important}.g-purple--500{color:#6835bb!important}.g-purple--700{color:#5026ab!important}.g-purple--900{color:#321197!important}.g-bg-purple--100{background-color:#cac0f7!important}.g-bg-purple--300{background-color:#8470db!important}.g-bg-purple--500{background-color:#6835bb!important}.g-bg-purple--700{background-color:#5026ab!important}.g-bg-purple--900{background-color:#321197!important}.g-violet--100{color:#f0d7f4!important}.g-violet--300{color:#f3c8f3!important}.g-violet--500{color:#f07ff0!important}.g-violet--700{color:#9d1bb1!important}.g-violet--900{color:#7b13a4!important}.g-bg-violet--100{background-color:#f0d7f4!important}.g-bg-violet--300{background-color:#f3c8f3!important}.g-bg-violet--500{background-color:#f07ff0!important}.g-bg-violet--700{background-color:#9d1bb1!important}.g-bg-violet--900{background-color:#7b13a4!important}.g-magenta--100{color:#ff42ff!important}.g-magenta--300{color:#f0f!important}.g-magenta--500{color:#d100d1!important}.g-magenta--700{color:purple!important}.g-magenta--900{color:#802182!important}.g-bg-magenta--100{background-color:#ff42ff!important}.g-bg-magenta--300{background-color:#f0f!important}.g-bg-magenta--500{background-color:#d100d1!important}.g-bg-magenta--700{background-color:purple!important}.g-bg-magenta--900{background-color:#802182!important}.g-gray--100{color:#f5f5f5!important}.g-gray--300{color:#e0e0e0!important}.g-gray--500{color:#9e9e9e!important}.g-gray--700{color:#616161!important}.g-gray--900{color:#212121!important}.g-bg-gray--100{background-color:#f5f5f5!important}.g-bg-gray--300{background-color:#e0e0e0!important}.g-bg-gray--500{background-color:#9e9e9e!important}.g-bg-gray--700{background-color:#616161!important}.g-bg-gray--900{background-color:#212121!important}.g-warm-gray--100{color:#ece9e4!important}.g-warm-gray--300{color:#cec4ba!important}.g-warm-gray--500{color:#c0b4a5!important}.g-warm-gray--700{color:#7e7467!important}.g-warm-gray--900{color:#36312b!important}.g-bg-warm-gray--100{background-color:#ece9e4!important}.g-bg-warm-gray--300{background-color:#cec4ba!important}.g-bg-warm-gray--500{background-color:#c0b4a5!important}.g-bg-warm-gray--700{background-color:#7e7467!important}.g-bg-warm-gray--900{background-color:#36312b!important}.g-cool-gray--100{color:#d0d9dd!important}.g-cool-gray--300{color:#8ea3af!important}.g-cool-gray--500{color:#5f7d8c!important}.g-cool-gray--700{color:#445a64!important}.g-cool-gray--900{color:#253137!important}.g-bg-cool-gray--100{background-color:#d0d9dd!important}.g-bg-cool-gray--300{background-color:#8ea3af!important}.g-bg-cool-gray--500{background-color:#5f7d8c!important}.g-bg-cool-gray--700{background-color:#445a64!important}.g-bg-cool-gray--900{background-color:#253137!important}.g-white{color:#fff!important}.g-black{color:#000!important}.g-nearWhite{color:#fafafa!important}.g-nearBlack{color:rgba(0,0,0,.87)!important}.g-bg-white{background-color:#fff!important}.g-bg-black{background-color:#000!important}.g-bg-nearWhite{background-color:#fafafa!important}.g-bg-nearBlack{background-color:rgba(0,0,0,.87)!important}.g-bg-transparent{background-color:transparent!important}.g-currentColor{color:currentColor!important}.g-bg-currentColor{background-color:currentColor!important}.g-border-none{border:0}.g-border-noRounding{border-radius:0}.g-border-circle{border-radius:50%}.g-border-s1{border-style:solid;border-width:1px;border-width:.1rem}.g-border-top-s1{border-top-style:solid;border-top-width:1px;border-top-width:.1rem}.g-border-right-s1{border-right-style:solid;border-right-width:1px;border-right-width:.1rem}.g-border-bottom-s1{border-bottom-style:solid;border-bottom-width:1px;border-bottom-width:.1rem}.g-border-left-s1{border-left-style:solid;border-left-width:1px;border-left-width:.1rem}.g-border-dash1{border-style:dashed;border-width:1px;border-width:.1rem}.g-border-top-dash1{border-top-style:dashed;border-top-width:1px;border-top-width:.1rem}.g-border-right-dash1{border-right-style:dashed;border-right-width:1px;border-right-width:.1rem}.g-border-bottom-dash1{border-bottom-style:dashed;border-bottom-width:1px;border-bottom-width:.1rem}.g-border-left-dash1{border-left-style:dashed;border-left-width:1px;border-left-width:.1rem}.g-border-s2{border-style:solid;border-width:3px;border-width:.2rem}.g-border-top-s2{border-top-style:solid;border-top-width:3px;border-top-width:.2rem}.g-border-right-s2{border-right-style:solid;border-right-width:3px;border-right-width:.2rem}.g-border-bottom-s2{border-bottom-style:solid;border-bottom-width:3px;border-bottom-width:.2rem}.g-border-left-s2{border-left-style:solid;border-left-width:3px;border-left-width:.2rem}.g-border-dash2{border-style:dashed;border-width:3px;border-width:.2rem}.g-border-top-dash2{border-top-style:dashed;border-top-width:3px;border-top-width:.2rem}.g-border-right-dash2{border-right-style:dashed;border-right-width:3px;border-right-width:.2rem}.g-border-bottom-dash2{border-bottom-style:dashed;border-bottom-width:3px;border-bottom-width:.2rem}.g-border-left-dash2{border-left-style:dashed;border-left-width:3px;border-left-width:.2rem}.g-border-s3{border-style:solid;border-width:4px;border-width:.3rem}.g-border-top-s3{border-top-style:solid;border-top-width:4px;border-top-width:.3rem}.g-border-right-s3{border-right-style:solid;border-right-width:4px;border-right-width:.3rem}.g-border-bottom-s3{border-bottom-style:solid;border-bottom-width:4px;border-bottom-width:.3rem}.g-border-left-s3{border-left-style:solid;border-left-width:4px;border-left-width:.3rem}.g-border-dash3{border-style:dashed;border-width:4px;border-width:.3rem}.g-border-top-dash3{border-top-style:dashed;border-top-width:4px;border-top-width:.3rem}.g-border-right-dash3{border-right-style:dashed;border-right-width:4px;border-right-width:.3rem}.g-border-bottom-dash3{border-bottom-style:dashed;border-bottom-width:4px;border-bottom-width:.3rem}.g-border-left-dash3{border-left-style:dashed;border-left-width:4px;border-left-width:.3rem}.g-border-s4{border-style:solid;border-width:6px;border-width:.4rem}.g-border-top-s4{border-top-style:solid;border-top-width:6px;border-top-width:.4rem}.g-border-right-s4{border-right-style:solid;border-right-width:6px;border-right-width:.4rem}.g-border-bottom-s4{border-bottom-style:solid;border-bottom-width:6px;border-bottom-width:.4rem}.g-border-left-s4{border-left-style:solid;border-left-width:6px;border-left-width:.4rem}.g-border-dash4{border-style:dashed;border-width:6px;border-width:.4rem}.g-border-top-dash4{border-top-style:dashed;border-top-width:6px;border-top-width:.4rem}.g-border-right-dash4{border-right-style:dashed;border-right-width:6px;border-right-width:.4rem}.g-border-bottom-dash4{border-bottom-style:dashed;border-bottom-width:6px;border-bottom-width:.4rem}.g-border-left-dash4{border-left-style:dashed;border-left-width:6px;border-left-width:.4rem}.g-border-s5{border-style:solid;border-width:8px;border-width:.5rem}.g-border-top-s5{border-top-style:solid;border-top-width:8px;border-top-width:.5rem}.g-border-right-s5{border-right-style:solid;border-right-width:8px;border-right-width:.5rem}.g-border-bottom-s5{border-bottom-style:solid;border-bottom-width:8px;border-bottom-width:.5rem}.g-border-left-s5{border-left-style:solid;border-left-width:8px;border-left-width:.5rem}.g-border-dash5{border-style:dashed;border-width:8px;border-width:.5rem}.g-border-top-dash5{border-top-style:dashed;border-top-width:8px;border-top-width:.5rem}.g-border-right-dash5{border-right-style:dashed;border-right-width:8px;border-right-width:.5rem}.g-border-bottom-dash5{border-bottom-style:dashed;border-bottom-width:8px;border-bottom-width:.5rem}.g-border-left-dash5{border-left-style:dashed;border-left-width:8px;border-left-width:.5rem}.g-box-shadow-1{box-shadow:0 1.5px 4px rgba(0,0,0,.24),0 1.5px 6px rgba(0,0,0,.12)}.g-box-shadow-2{box-shadow:0 3px 12px rgba(0,0,0,.23),0 3px 12px rgba(0,0,0,.16)}.g-box-shadow-3{box-shadow:0 10px 12px rgba(0,0,0,.23),0 6px 40px rgba(0,0,0,.19)}.g-box-shadow-4{box-shadow:0 14px 20px rgba(0,0,0,.22),0 10px 56px rgba(0,0,0,.25)}.g-box-shadow-5{box-shadow:0 19px 24px rgba(0,0,0,.22),0 15px 76px rgba(0,0,0,.3)}.g-font-100{font-weight:100}.g-font-200{font-weight:200}.g-font-300{font-weight:300}.g-font-400{font-weight:400}.g-font-500{font-weight:500}.g-font-600{font-weight:600}.g-font-700{font-weight:700}.g-font-800{font-weight:800}.g-font-900{font-weight:900}.g-font-smaller{font-size:.83333em}.g-font-larger{font-size:1.2em}.g-small-caps{font-size:.666em;text-transform:uppercase;letter-spacing:.125em}.g-font-size--halfRoot{font-size:8px;font-size:.5rem}.g-font-size--threeQuartersRoot{font-size:12px;font-size:.75rem}.g-font-size--root{font-size:16px;font-size:1rem}.g-font-size--rootPlusQuarter{font-size:20px;font-size:1.25rem}.g-font-size--rootPlusThird{font-size:21px;font-size:1.333333rem}.g-font-size--rootPlusHalf{font-size:24px;font-size:1.5rem}.g-font-size--twoRoot{font-size:32px;font-size:2rem}.g-truncate{max-width:100%;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.g-clip{max-width:100%;text-overflow:clip;white-space:nowrap;overflow:hidden}.g-link-reset{background-image:none;text-decoration:none}.g-link-reset:hover,.g-link-reset:focus{background-image:none;text-decoration:none;color:currentColor}.g-list-reset{padding-left:0;list-style:none}.g-list-style-none{list-style:none}.g-underline{text-decoration:underline}.g-strikethrough{text-decoration:line-through}.u-align-left{text-align:left!important}.u-align-right{text-align:right!important}.u-align-center{text-align:center!important}.u-b-box{box-sizing:border-box}.u-c-box{box-sizing:content-box}.u-pad-box{box-sizing:padding-box}.u-pointer{cursor:pointer!important}.u-block{display:block}.u-inline-block{display:inline-block}.u-inline{display:inline}.u-justify-start{-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}.u-justify-end{-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}.u-justify-center{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.u-overflow-auto{overflow:auto}.u-overflow-auto-x{overflow-x:auto}.u-overflow-auto-y{overflow-y:auto}.u-overflow-hide{overflow:hidden}.u-overflow-hide-x{overflow-x:hidden}.u-overflow-hide-y{overflow-y:hidden}.u-overflow-show{overflow:visible}.u-overflow-show-y{overflow-y:visible}.u-overflow-show-x{overflow-y:visible}.u-overflow-scroll-y{overflow-y:scroll}.u-overflow-scroll-x{overflow-x:scroll}.u-static{position:static}.u-relative{position:relative}.u-absolute{position:absolute}.u-transform-center{top:50%;left:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.u-offset-left{top:0;left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.u-fixed{position:fixed}.u-flow-middle{margin-left:auto;margin-right:auto}.u-sticky{position:-webkit-sticky;position:sticky}.u-edge-top{top:0}.u-edge-bottom{bottom:0}.u-edge-left{left:0}.u-edge-right{right:0}.u-full-bleed{height:100vh;margin:0}.u-full-stretch{width:100vw;margin:0}.u-fill-height{height:100%}.u-inherit-height{height:inherit}.u-fit-width{max-width:100%}.u-fill-width{width:100%}.u-inherit-width{width:inherit}.u-width-95{width:95%}.u-width-90{width:90%}.u-width-85{width:85%}.u-width-80{width:80%}.u-width-75,.u-width-threeQuarters{width:75%}.u-width-70{width:70%}.u-width-twoThirds{width:66.6667%}.u-width-60{width:60%}.u-width-55{width:55%}.u-width-50,.u-width-oneHalf{width:50%}.u-width-45{width:45%}.u-width-40{width:40%}.u-width-35{width:35%}.u-width-oneThird{width:33.3333%}.u-width-30{width:30%}.u-width-25,.u-width-oneQuarter{width:25%}.u-width-20{width:20%}.u-width-15{width:15%}.u-width-10{width:10%}.u-height-90{height:90%}.u-height-80{height:80%}.u-height-70{height:70%}.u-height-60{height:60%}.u-height-50{height:50%}.u-height-40{height:40%}.u-height-30{height:30%}.u-height-20{height:20%}.u-height-10{height:10%}.u-width-spacing-1{width:8px;width:.5rem}.u-width-spacing-2{width:16px;width:1rem}.u-width-spacing-3{width:32px;width:2rem}.u-width-spacing-4{width:64px;width:4rem}.u-width-spacing-5{width:128px;width:8rem}.u-height-spacing-1{height:8px;height:.5rem}.u-height-spacing-2{height:16px;height:1rem}.u-height-spacing-3{height:32px;height:2rem}.u-height-spacing-4{height:64px;height:4rem}.u-height-spacing-5{height:128px;height:8rem}.u-min-width-breakpoint--xs{min-width:20em}.u-min-width-breakpoint--sm{min-width:32em}.u-min-width-breakpoint--md{min-width:48em}.u-min-width-breakpoint--lg{min-width:64em}.u-min-width-breakpoint--xl{min-width:80em}.u-min-width-breakpoint--xxl{min-width:96em}.u-p0{padding:0}.u-p1{padding:8px;padding:.5rem}.u-p2{padding:16px;padding:1rem}.u-p3{padding:32px;padding:2rem}.u-p4{padding:64px;padding:4rem}.u-p5{padding:128px;padding:8rem}.u-pt0{padding-top:0}.u-pt1{padding-top:8px;padding-top:.5rem}.u-pt2{padding-top:16px;padding-top:1rem}.u-pt3{padding-top:32px;padding-top:2rem}.u-pt4{padding-top:64px;padding-top:4rem}.u-pt5{padding-top:128px;padding-top:8rem}.u-pr0{padding-right:0}.u-pr1{padding-right:8px;padding-right:.5rem}.u-pr2{padding-right:16px;padding-right:1rem}.u-pr3{padding-right:32px;padding-right:2rem}.u-pr4{padding-right:64px;padding-right:4rem}.u-pr5{padding-right:128px;padding-right:8rem}.u-pb0{padding-bottom:0}.u-pb1{padding-bottom:8px;padding-bottom:.5rem}.u-pb2{padding-bottom:16px;padding-bottom:1rem}.u-pb3{padding-bottom:32px;padding-bottom:2rem}.u-pb4{padding-bottom:64px;padding-bottom:4rem}.u-pb5{padding-bottom:128px;padding-bottom:8rem}.u-pl0{padding-left:0}.u-pl1{padding-left:8px;padding-left:.5rem}.u-pl2{padding-left:16px;padding-left:1rem}.u-pl3{padding-left:32px;padding-left:2rem}.u-pl4{padding-left:64px;padding-left:4rem}.u-pl5{padding-left:128px;padding-left:8rem}.u-m-auto{margin:auto}.u-m0{margin:0}.u-m1{margin:8px;margin:.5rem}.u-m2{margin:16px;margin:1rem}.u-m3{margin:32px;margin:2rem}.u-m4{margin:64px;margin:4rem}.u-m5{margin:128px;margin:8rem}.u-mt-auto{margin-top:auto}.u-mt0{margin-top:0}.u-mt1{margin-top:8px;margin-top:.5rem}.u-mt2{margin-top:16px;margin-top:1rem}.u-mt3{margin-top:32px;margin-top:2rem}.u-mt4{margin-top:64px;margin-top:4rem}.u-mt5{margin-top:128px;margin-top:8rem}.u-mr-auto{margin-right:auto}.u-mr0{margin-right:0}.u-mr1{margin-right:8px;margin-right:.5rem}.u-mr2{margin-right:16px;margin-right:1rem}.u-mr3{margin-right:32px;margin-right:2rem}.u-mr4{margin-right:64px;margin-right:4rem}.u-mr5{margin-right:128px;margin-right:8rem}.u-mb-auto{margin-bottom:auto}.u-mb0{margin-bottom:0}.u-mb1{margin-bottom:8px;margin-bottom:.5rem}.u-mb2{margin-bottom:16px;margin-bottom:1rem}.u-mb3{margin-bottom:32px;margin-bottom:2rem}.u-mb4{margin-bottom:64px;margin-bottom:4rem}.u-mb5{margin-bottom:128px;margin-bottom:8rem}.u-ml-auto{margin-left:auto}.u-ml0{margin-left:0}.u-ml1{margin-left:8px;margin-left:.5rem}.u-ml2{margin-left:16px;margin-left:1rem}.u-ml3{margin-left:32px;margin-left:2rem}.u-ml4{margin-left:64px;margin-left:4rem}.u-ml5{margin-left:128px;margin-left:8rem}.u-list-reset{padding-left:0;list-style:none}.u-nowrap{white-space:nowrap}.u-break-word{white-space:break-word}.u-zn6{z-index:-6}.u-zn5{z-index:-5}.u-zn4{z-index:-4}.u-zn3{z-index:-3}.u-zn2{z-index:-2}.u-zn1{z-index:-1}.u-z0{z-index:0}.u-z1{z-index:1}.u-z2{z-index:2}.u-z3{z-index:3}.u-z4{z-index:4}.u-z5{z-index:5}.u-z6{z-index:6}@media screen and (min-width:64em){blockquote{font-size:1.44em;line-height:1em}body{font-size:1.44em}h1{font-size:2.98598em}h2{font-size:2.48832em}h3{font-size:2.0736em}h4{font-size:1.728em}.o-flex-grid._lg-o-flex-grid--gutters{margin:-1em 0 0 -1em}.o-flex-grid._lg-o-flex-grid--gutters>.o-grid-cell{padding:1em 0 0 1em}.o-flex-grid._lg-o-flex-grid--guttersLg{margin:-1.5em 0 0 -1.5em}.o-flex-grid._lg-o-flex-grid--guttersLg>.o-grid-cell{padding:1.5em 0 0 1.5em}.o-flex-grid._lg-o-flex-grid--guttersXl{margin:-2em 0 0 -2em}.o-flex-grid._lg-o-flex-grid--guttersXl>.o-grid-cell{padding:2em 0 0 2em}.o-flex-grid._lg-o-flex-grid--fit>.o-grid-cell{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.o-flex-grid._lg-o-flex-grid--full>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.o-flex-grid._lg-o-flex-grid--oneHalf>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.o-flex-grid._lg-o-flex-grid--oneThird>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 33.33333%;-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.333333%}.o-flex-grid._lg-o-flex-grid--oneQuarter>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}._lg-o-list--unflattening{list-style-type:disc;padding-left:3em}._lg-o-list--unflattening>li{display:list-item;list-style-type:disc}._lg-u-align-left{text-align:left!important}._lg-u-align-right{text-align:right!important}._lg-u-align-center{text-align:center!important}._lg-u-block{display:block}._lg-u-inline-block{display:inline-block}._lg-u-inline{display:inline}._lg-u-justify-start{-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}._lg-u-justify-end{-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}._lg-u-justify-center{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}._lg-u-static{position:static}._lg-u-relative{position:relative}._lg-u-absolute{position:absolute}._lg-u-fixed{position:fixed}._lg-u-transform-center{top:50%;left:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}._lg-u-offset-left{top:0;left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}._lg-u-sticky{position:-webkit-sticky;position:sticky}._lg-u-flow-middle{margin-left:auto;margin-right:auto}._lg-u-edge-top{top:0}._lg-u-edge-bottom{bottom:0}._lg-u-edge-left{left:0}._lg-u-edge-right{right:0}._lg-u-p0{padding:0}._lg-u-p1{padding:.5rem}._lg-u-p2{padding:1rem}._lg-u-p3{padding:2rem}._lg-u-p4{padding:4rem}._lg-u-p5{padding:8rem}._lg-u-pt0{padding-top:0}._lg-u-pt1{padding-top:.5rem}._lg-u-pt2{padding-top:1rem}._lg-u-pt3{padding-top:2rem}._lg-u-pt4{padding-top:4rem}._lg-u-pt5{padding-top:8rem}._lg-u-pr0{padding-right:0}._lg-u-pr1{padding-right:.5rem}._lg-u-pr2{padding-right:1rem}._lg-u-pr3{padding-right:2rem}._lg-u-pr4{padding-right:4rem}._lg-u-pr5{padding-right:8rem}._lg-u-pb0{padding-bottom:0}._lg-u-pb1{padding-bottom:.5rem}._lg-u-pb2{padding-bottom:1rem}._lg-u-pb3{padding-bottom:2rem}._lg-u-pb4{padding-bottom:4rem}._lg-u-pb5{padding-bottom:8rem}._lg-u-pl0{padding-left:0}._lg-u-pl1{padding-left:.5rem}._lg-u-pl2{padding-left:1rem}._lg-u-pl3{padding-left:2rem}._lg-u-pl4{padding-left:4rem}._lg-u-pl5{padding-left:8rem}._lg-u-m-auto{margin:auto}._lg-u-m0{margin:0}._lg-u-m1{margin:.5rem}._lg-u-m2{margin:1rem}._lg-u-m3{margin:2rem}._lg-u-m4{margin:4rem}._lg-u-m5{margin:8rem}._lg-u-mt-auto{margin-top:auto}._lg-u-mt0{margin-top:0}._lg-u-mt1{margin-top:.5rem}._lg-u-mt2{margin-top:1rem}._lg-u-mt3{margin-top:2rem}._lg-u-mt4{margin-top:4rem}._lg-u-mt5{margin-top:8rem}._lg-u-mr-auto{margin-right:auto}._lg-u-mr0{margin-right:0}._lg-u-mr1{margin-right:.5rem}._lg-u-mr2{margin-right:1rem}._lg-u-mr3{margin-right:2rem}._lg-u-mr4{margin-right:4rem}._lg-u-mr5{margin-right:8rem}._lg-u-mb-auto{margin-bottom:auto}._lg-u-mb0{margin-bottom:0}._lg-u-mb1{margin-bottom:.5rem}._lg-u-mb2{margin-bottom:1rem}._lg-u-mb3{margin-bottom:2rem}._lg-u-mb4{margin-bottom:4rem}._lg-u-mb5{margin-bottom:8rem}._lg-u-ml-auto{margin-left:auto}._lg-u-ml0{margin-left:0}._lg-u-ml1{margin-left:.5rem}._lg-u-ml2{margin-left:1rem}._lg-u-ml3{margin-left:2rem}._lg-u-ml4{margin-left:4rem}._lg-u-ml5{margin-left:8rem}}@media screen and (min-width:20em){body{font-size:1em}}@media screen and (min-width:32em){body{font-size:1.2em}.o-flex-grid._ns-o-flex-grid--gutters{margin:-1em 0 0 -1em}.o-flex-grid._ns-o-flex-grid--gutters>.o-grid-cell{padding:1em 0 0 1em}.o-flex-grid._ns-o-flex-grid--guttersLg{margin:-1.5em 0 0 -1.5em}.o-flex-grid._ns-o-flex-grid--guttersLg>.o-grid-cell{padding:1.5em 0 0 1.5em}.o-flex-grid._ns-o-flex-grid--guttersXl{margin:-2em 0 0 -2em}.o-flex-grid._ns-o-flex-grid--guttersXl>.o-grid-cell{padding:2em 0 0 2em}.o-flex-grid._ns-o-flex-grid--fit>.o-grid-cell{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.o-flex-grid._ns-o-flex-grid--full>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.o-flex-grid._ns-o-flex-grid--oneHalf>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.o-flex-grid._ns-o-flex-grid--oneThird>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 33.33333%;-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.333333333%}.o-flex-grid._ns-o-flex-grid--oneQuarter>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}._ns-o-list--unflattening{list-style-type:disc;padding-left:1.25em}._ns-o-list--unflattening>li{display:list-item;list-style-type:disc}._ns-u-align-left{text-align:left!important}._ns-u-align-right{text-align:right!important}._ns-u-align-center{text-align:center!important}._ns-u-block{display:block}._ns-u-inline-block{display:inline-block}._ns-u-inline{display:inline}._ns-u-justify-start{-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}._ns-u-justify-end{-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}._ns-u-justify-center{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}._ns-u-static{position:static}._ns-u-relative{position:relative}._ns-u-absolute{position:absolute}._ns-u-fixed{position:fixed}._ns-u-offset-left{top:0;left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}._ns-u-transform-center{top:50%;left:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}._ns-u-sticky{position:-webkit-sticky;position:sticky}._ns-u-flow-middle{margin-left:auto;margin-right:auto}._ns-u-edge-top{top:0}._ns-u-edge-bottom{bottom:0}._ns-u-edge-left{left:0}._ns-u-edge-right{right:0}._ns-u-p0{padding:0}._ns-u-p1{padding:.5rem}._ns-u-p2{padding:1rem}._ns-u-p3{padding:2rem}._ns-u-p4{padding:4rem}._ns-u-p5{padding:8rem}._ns-u-pt0{padding-top:0}._ns-u-pt1{padding-top:.5rem}._ns-u-pt2{padding-top:1rem}._ns-u-pt3{padding-top:2rem}._ns-u-pt4{padding-top:4rem}._ns-u-pt5{padding-top:8rem}._ns-u-pr0{padding-right:0}._ns-u-pr1{padding-right:.5rem}._ns-u-pr2{padding-right:1rem}._ns-u-pr3{padding-right:2rem}._ns-u-pr4{padding-right:4rem}._ns-u-pr5{padding-right:8rem}._ns-u-pb0{padding-bottom:0}._ns-u-pb1{padding-bottom:.5rem}._ns-u-pb2{padding-bottom:1rem}._ns-u-pb3{padding-bottom:2rem}._ns-u-pb4{padding-bottom:4rem}._ns-u-pb5{padding-bottom:8rem}._ns-u-pl0{padding-left:0}._ns-u-pl1{padding-left:.5rem}._ns-u-pl2{padding-left:1rem}._ns-u-pl3{padding-left:2rem}._ns-u-pl4{padding-left:4rem}._ns-u-pl5{padding-left:8rem}._ns-u-m-auto{margin:auto}._ns-u-m0{margin:0}._ns-u-m1{margin:.5rem}._ns-u-m2{margin:1rem}._ns-u-m3{margin:2rem}._ns-u-m4{margin:4rem}._ns-u-m5{margin:8rem}._ns-u-mt-auto{margin-top:auto}._ns-u-mt0{margin-top:0}._ns-u-mt1{margin-top:.5rem}._ns-u-mt2{margin-top:1rem}._ns-u-mt3{margin-top:2rem}._ns-u-mt4{margin-top:4rem}._ns-u-mt5{margin-top:8rem}._ns-u-mr-auto{margin-right:auto}._ns-u-mr0{margin-right:0}._ns-u-mr1{margin-right:.5rem}._ns-u-mr2{margin-right:1rem}._ns-u-mr3{margin-right:2rem}._ns-u-mr4{margin-right:4rem}._ns-u-mr5{margin-right:8rem}._ns-u-mb-auto{margin-bottom:auto}._ns-u-mb0{margin-bottom:0}._ns-u-mb1{margin-bottom:.5rem}._ns-u-mb2{margin-bottom:1rem}._ns-u-mb3{margin-bottom:2rem}._ns-u-mb4{margin-bottom:4rem}._ns-u-mb5{margin-bottom:8rem}._ns-u-ml-auto{margin-left:auto}._ns-u-ml0{margin-left:0}._ns-u-ml1{margin-left:.5rem}._ns-u-ml2{margin-left:1rem}._ns-u-ml3{margin-left:2rem}._ns-u-ml4{margin-left:4rem}._ns-u-ml5{margin-left:8rem}}@media screen and (min-width:undefined){body{font-size:1.728em}h1{font-size:3.58318em}h2{font-size:2.98598em}h3{font-size:2.48832em}h4{font-size:2.0736em}}@media screen and (min-width:48em){h1{font-size:2.48832em}h2{font-size:2.0736em}h3{font-size:1.728em}h4{font-size:1.44em}dd{margin-left:2em}ul{padding-left:3em}ol{padding-left:3em}.o-flex-grid._md-o-flex-grid--gutters{margin:-1em 0 0 -1em}.o-flex-grid._md-o-flex-grid--gutters>.o-grid-cell{padding:1em 0 0 1em}.o-flex-grid._md-o-flex-grid--guttersLg{margin:-1.5em 0 0 -1.5em}.o-flex-grid._md-o-flex-grid--guttersLg>.o-grid-cell{padding:1.5em 0 0 1.5em}.o-flex-grid._md-o-flex-grid--guttersXl{margin:-2em 0 0 -2em}.o-flex-grid._md-o-flex-grid--guttersXl>.o-grid-cell{padding:2em 0 0 2em}.o-flex-grid._md-o-flex-grid--fit>.o-grid-cell{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.o-flex-grid._md-o-flex-grid--full>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.o-flex-grid._md-o-flex-grid--oneHalf>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.o-flex-grid._md-o-flex-grid--oneThird>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 33.33333%;-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.o-flex-grid._md-o-flex-grid--oneQuarter>.o-grid-cell{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}._md-o-list--unflattening{list-style-type:disc;padding-left:3em}._md-o-list--unflattening>li{display:list-item;list-style-type:disc}._md-u-align-left{text-align:left!important}._md-u-align-right{text-align:right!important}._md-u-align-center{text-align:center!important}._md-u-block{display:block}._md-u-inline-block{display:inline-block}._md-u-inline{display:inline}._md-u-justify-start{-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}._md-u-justify-end{-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}._md-u-justify-center{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}._md-u-static{position:static}._md-u-relative{position:relative}._md-u-absolute{position:absolute}._md-u-fixed{position:fixed}._md-u-transform-center{top:50%;left:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}._md-u-offset-left{top:0;left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}._md-u-sticky{position:-webkit-sticky;position:sticky}._md-u-flow-middle{margin-left:auto;margin-right:auto}._md-u-edge-top{top:0}._md-u-edge-bottom{bottom:0}._md-u-edge-left{left:0}._md-u-edge-right{right:0}._md-u-p0{padding:0}._md-u-p1{padding:.5rem}._md-u-p2{padding:1rem}._md-u-p3{padding:2rem}._md-u-p4{padding:4rem}._md-u-p5{padding:8rem}._md-u-pt0{padding-top:0}._md-u-pt1{padding-top:.5rem}._md-u-pt2{padding-top:1rem}._md-u-pt3{padding-top:2rem}._md-u-pt4{padding-top:4rem}._md-u-pt5{padding-top:8rem}._md-u-pr0{padding-right:0}._md-u-pr1{padding-right:.5rem}._md-u-pr2{padding-right:1rem}._md-u-pr3{padding-right:2rem}._md-u-pr4{padding-right:4rem}._md-u-pr5{padding-right:8rem}._md-u-pb0{padding-bottom:0}._md-u-pb1{padding-bottom:.5rem}._md-u-pb2{padding-bottom:1rem}._md-u-pb3{padding-bottom:2rem}._md-u-pb4{padding-bottom:4rem}._md-u-pb5{padding-bottom:8rem}._md-u-pl0{padding-left:0}._md-u-pl1{padding-left:.5rem}._md-u-pl2{padding-left:1rem}._md-u-pl3{padding-left:2rem}._md-u-pl4{padding-left:4rem}._md-u-pl5{padding-left:8rem}._md-u-m-auto{margin:auto}._md-u-m0{margin:0}._md-u-m1{margin:.5rem}._md-u-m2{margin:1rem}._md-u-m3{margin:2rem}._md-u-m4{margin:4rem}._md-u-m5{margin:8rem}._md-u-mt-auto{margin-top:auto}._md-u-mt0{margin-top:0}._md-u-mt1{margin-top:.5rem}._md-u-mt2{margin-top:1rem}._md-u-mt3{margin-top:2rem}._md-u-mt4{margin-top:4rem}._md-u-mt5{margin-top:8rem}._md-u-mr-auto{margin-right:auto}._md-u-mr0{margin-right:0}._md-u-mr1{margin-right:.5rem}._md-u-mr2{margin-right:1rem}._md-u-mr3{margin-right:2rem}._md-u-mr4{margin-right:4rem}._md-u-mr5{margin-right:8rem}._md-u-mb-auto{margin-bottom:auto}._md-u-mb0{margin-bottom:0}._md-u-mb1{margin-bottom:.5rem}._md-u-mb2{margin-bottom:1rem}._md-u-mb3{margin-bottom:2rem}._md-u-mb4{margin-bottom:4rem}._md-u-mb5{margin-bottom:8rem}._md-u-ml-auto{margin-left:auto}._md-u-ml0{margin-left:0}._md-u-ml1{margin-left:.5rem}._md-u-ml2{margin-left:1rem}._md-u-ml3{margin-left:2rem}._md-u-ml4{margin-left:4rem}._md-u-ml5{margin-left:8rem}}@media screen and (min-width:80em){._xl-u-align-left{text-align:left!important}._xl-u-align-right{text-align:right!important}._xl-u-align-center{text-align:center!important}._xl-u-justify-start{-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}._xl-u-justify-end{-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}._xl-u-justify-center{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}._xlg-u-static{position:static}._xlg-u-relative{position:relative}._xlg-u-absolute{position:absolute}._xlg-u-fixed{position:fixed}._xlg-u-transform-center{top:50%;left:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}._xlg-u-offset-left{top:0;left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}._xlg-u-sticky{position:-webkit-sticky;position:sticky}._xlg-u-flow-middle{margin-left:auto;margin-right:auto}._xlg-u-edge-top{top:0}._xlg-u-edge-bottom{bottom:0}._xlg-u-edge-left{left:0}._xlg-u-edge-right{right:0}} --------------------------------------------------------------------------------