Polymer .9: show basic state cards

pull/132/head
Paulus Schoutsen 2015-05-17 22:08:42 -07:00
parent 313eb71b17
commit 4576a1d245
14 changed files with 574 additions and 333 deletions

View File

@ -1,24 +1,38 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="state-card-display.html">
<link rel="import" href="state-card-toggle.html">
<!-- <link rel="import" href="state-card-toggle.html">
<link rel="import" href="state-card-thermostat.html">
<link rel="import" href="state-card-configurator.html">
<link rel="import" href="state-card-scene.html">
<link rel="import" href="state-card-scene.html"> -->
<polymer-element name="state-card-content" attributes="stateObj">
<template>
<dom-module is="state-card-content">
<style>
:host {
display: block;
}
</style>
<template>
<state-card-display state-obj="[[stateObj]]"></state-card-display>
<!-- <div id='cardContainer'></div> -->
</template>
</dom-module>
<div id='cardContainer'></div>
</template>
<script>
Polymer({
is: 'state-card-content',
properties: {
stateObj: {
type: Object,
value: {},
observe: 'stateObjChanged',
}
},
stateObjChanged: function(oldVal, newVal) {
console.log('stateObjChanged');
return;
var cardContainer = this.$.cardContainer;
if (!newVal) {
@ -45,4 +59,3 @@ Polymer({
},
});
</script>
</polymer-element>

View File

@ -2,8 +2,7 @@
<link rel="import" href="../components/state-info.html">
<polymer-element name="state-card-display" attributes="stateObj" noscript>
<template>
<dom-module is="state-card-display">
<style>
.state {
margin-left: 16px;
@ -14,9 +13,24 @@
}
</style>
<div horizontal justified layout>
<state-info stateObj="{{stateObj}}"></state-info>
<div class='state'>{{stateObj.stateDisplay}}</div>
<template>
<div class='horizontal justified layout'>
<state-info state-obj="[[stateObj]]"></state-info>
<div class='state'>[[stateObj.stateDisplay]]</div>
</div>
</template>
</polymer-element>
</dom-module>
<script>
(function() {
Polymer({
is: 'state-card-display',
properties: {
stateObj: {
type: Object,
},
},
});
})();
</script>

View File

@ -2,8 +2,7 @@
<link rel="import" href="state-card-content.html">
<polymer-element name="state-card" attributes="stateObj" on-click="cardClicked">
<template>
<dom-module is="state-card">
<style>
:host {
border-radius: 2px;
@ -19,15 +18,31 @@
}
</style>
<state-card-content stateObj={{stateObj}}></state-card-content>
</template>
<script>
var uiActions = window.hass.uiActions;
<template>
<state-card-content state-obj="[[stateObj]]"></state-card-content>
</template>
</dom-module>
<script>
(function(){
var uiActions = window.hass.uiActions;
Polymer({
is: 'state-card',
properties: {
stateObj: {
type: Object,
},
},
listeners: {
'click': 'cardClicked',
},
Polymer({
cardClicked: function() {
uiActions.showMoreInfoDialog(this.stateObj.entityId);
},
});
});
})();
</script>
</polymer-element>

View File

@ -1,24 +1,37 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-icon/iron-icon.html">
<link rel="import" href="../resources/home-assistant-icons.html">
<polymer-element name="domain-icon"
attributes="domain state" constructor="DomainIcon">
<dom-module is="domain-icon">
<template>
<core-icon icon="{{icon}}"></core-icon>
<iron-icon icon="[[computeIcon(domain, state)]]"></iron-icon>
</template>
<script>
Polymer({
icon: '',
</dom-module>
observe: {
'domain': 'updateIcon',
'state' : 'updateIcon',
<script>
(function() {
var uiUtil = window.hass.uiUtil;
Polymer({
is: 'domain-icon',
properties: {
domain: {
type: String,
value: '',
},
updateIcon: function() {
this.icon = window.hass.uiUtil.domainIcon(this.domain, this.state);
state: {
type: String,
value: '',
},
},
computeIcon: function(domain, state) {
return uiUtil.domainIcon(domain, state);
},
});
</script>
</polymer-element>
})();
</script>

View File

@ -2,17 +2,42 @@
<link rel="import" href="../resources/moment-js.html">
<polymer-element name="relative-ha-datetime" attributes="datetime datetimeObj">
<dom-module is="relative-ha-datetime">
<template>
{{ relativeTime }}
<span>[[relativeTime]]</span>
</template>
<script>
(function() {
</dom-module>
<script>
(function() {
var UPDATE_INTERVAL = 60000; // 60 seconds
var parseDateTime = window.hass.util.parseDateTime;
Polymer({
is: 'relative-ha-datetime',
properties: {
datetime: {
type: String,
observe: 'datetimeChanged',
},
datetimeObj: {
type: Object,
observe: 'datetimeObjChanged',
},
parsedDateTime: {
type: Object,
},
relativeTime: {
type: String,
value: 'not set',
},
},
relativeTime: "",
parsedDateTime: null,
@ -22,6 +47,14 @@
attached: function() {
this._interval = setInterval(this.updateRelative, UPDATE_INTERVAL);
if (this.datetimeObj) {
this.parsedDateTime = this.datetimeObj;
} else if (this.datetime) {
this.parsedDateTime = parseDateTime(this.datetime);
}
this.updateRelative();
},
detached: function() {
@ -45,6 +78,5 @@
moment(this.parsedDateTime).fromNow() : "";
},
});
})();
</script>
</polymer-element>
})();
</script>

View File

@ -1,10 +1,9 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-image/core-image.html">
<link rel="import" href="../bower_components/iron-image/iron-image.html">
<link rel="import" href="domain-icon.html">
<polymer-element name="state-badge" attributes="stateObj">
<template>
<dom-module is="state-badge">
<style>
:host {
position: relative;
@ -21,7 +20,7 @@
text-align: center;
}
core-image {
iron-image {
border-radius: 50%;
}
@ -37,26 +36,30 @@
}
</style>
<div horizontal layout center>
<template>
<div class='layout horizontal center'>
<domain-icon id="icon"
domain="{{stateObj.domain}}" data-domain="{{stateObj.domain}}"
state="{{stateObj.state}}" data-state="{{stateObj.state}}">
domain="[[stateObj.domain]]" data-domain="[[stateObj.domain]]"
state="[[stateObj.state]]" data-state="[[stateObj.state]]">
</domain-icon>
<template if="{{stateObj.attributes.entity_picture}}">
<core-image
sizing="cover" fit
src="{{stateObj.attributes.entity_picture}}"></core-image>
<template is='dom-if' if="[[stateObj.attributes.entity_picture]]">
<iron-image
sizing="cover" class='fit'
src="[[stateObj.attributes.entity_picture]]"></iron-image>
</template>
</div>
</template>
<script>
Polymer({
observe: {
'stateObj.state': 'updateIconColor',
'stateObj.attributes.brightness': 'updateIconColor',
'stateObj.attributes.xy_color[0]': 'updateIconColor',
'stateObj.attributes.xy_color[1]': 'updateIconColor'
</dom-module>
<script>
Polymer({
is: 'state-badge',
properties: {
stateObj: {
type: Object,
observe: 'updateIconColor',
},
},
/**
@ -94,12 +97,11 @@
r /= maxValue;
g /= maxValue;
b /= maxValue;
r = r * 255; if (r < 0) { r = 255 };
g = g * 255; if (g < 0) { g = 255 };
b = b * 255; if (b < 0) { b = 255 };
return [r, g, b]
r = r * 255; if (r < 0) { r = 255; }
g = g * 255; if (g < 0) { g = 255; }
b = b * 255; if (b < 0) { b = 255; }
return [r, g, b];
}
});
</script>
</polymer-element>
});
</script>

View File

@ -2,8 +2,7 @@
<link rel="import" href="../cards/state-card.html">
<polymer-element name="state-cards" attributes="states" noscript>
<template>
<dom-module is="state-cards" attributes="states" noscript>
<style>
:host {
display: block;
@ -39,13 +38,14 @@
}
</style>
<div horizontal layout wrap>
<template>
<div class='horizontal layout wrap'>
<template repeat="{{states as state}}">
<state-card class="state-card" stateObj={{state}}></state-card>
<template is='dom-repeat' items="{{states}}">
<state-card class="state-card" state-obj="[[item]]"></state-card>
</template>
<template if="{{states.length == 0}}">
<template if="{{emptyStates}}">
<div class='no-states-content'>
<content></content>
</div>
@ -53,4 +53,28 @@
</div>
</template>
</polymer-element>
</dom-module>
<script>
(function() {
Polymer({
is: 'state-cards',
properties: {
states: {
type: Array,
value: [],
},
emptyStates: {
type: Boolean,
computed: 'computeEmptyStates(states)',
},
},
computeEmptyStates: function(states) {
return states.length == 0;
},
});
})();
</script>

View File

@ -1,12 +1,10 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-tooltip/core-tooltip.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<!-- <link rel="import" href="../bower_components/core-tooltip/core-tooltip.html"> -->
<link rel="import" href="state-badge.html">
<link rel="import" href="relative-ha-datetime.html">
<polymer-element name="state-info" attributes="stateObj" noscript>
<template>
<dom-module is="state-info">
<style>
state-badge {
float: left;
@ -28,21 +26,39 @@
}
</style>
<template>
<div>
<state-badge stateObj="{{stateObj}}"></state-badge>
<state-badge state-obj='[[stateObj]]'></state-badge>
<div class='info'>
<div class='name'>
{{stateObj.entityDisplay}}
</div>
<div class='name'>[[stateObj.entityDisplay]]</div>
<div class="time-ago">
<core-tooltip label="{{stateObj.lastChangedAsDate | formatDateTime}}" position="bottom">
<relative-ha-datetime datetimeObj="{{stateObj.lastChangedAsDate}}"></relative-ha-datetime>
</core-tooltip>
<div class='time-ago'>
<!-- <core-tooltip label="[[computeTooltipLabel(stateObj)]]" position="bottom"> -->
<relative-ha-datetime datetime-obj='[[stateObj.lastChangedAsDate]]'></relative-ha-datetime>
<!-- </core-tooltip> -->
</div>
</div>
</div>
</template>
</polymer-element>
</dom-module>
<script>
(function() {
Polymer({
is: 'state-info',
properties: {
stateObj: {
type: Object,
},
},
computeTooltipLabel: function(stateObj) {
// stateObj.lastChangedAsDate | formatDateTime
return 'Label TODO';
},
});
})();
</script>

View File

@ -9,8 +9,8 @@
<link rel="import" href="../bower_components/paper-item/paper-item.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<!-- <link rel="import" href="../layouts/partial-states.html">
<link rel="import" href="../layouts/partial-history.html">
<link rel="import" href="../layouts/partial-states.html">
<!-- <link rel="import" href="../layouts/partial-history.html">
<link rel="import" href="../layouts/partial-logbook.html">
<link rel="import" href="../layouts/partial-dev-fire-event.html">
<link rel="import" href="../layouts/partial-dev-call-service.html">
@ -56,7 +56,7 @@
<!-- <ha-modals></ha-modals> -->
<paper-drawer-panel id="drawer" on-paper-responsive-change="responsiveChanged">
<paper-header-panel mode="scroll" drawer class='sidenav layout fit'>
<paper-header-panel mode="scroll" drawer class='sidenav fit'>
<paper-toolbar>
Home Assistant
</paper-toolbar>
@ -148,12 +148,12 @@
This is the main partial, never remove it from the DOM but hide it
to speed up when people click on states.
-->
<!-- <partial-states hidden?="{{hideStates}}"
<partial-states hidden?="{{hideStates}}"
main narrow="{{narrow}}"
togglePanel="{{togglePanel}}"
filter="{{stateFilter}}">
</partial-states>
<!--
<template is='dom-if' if="{{selected == 'history'}}">
<partial-history main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-history>
</template>
@ -169,10 +169,6 @@
<template is='dom-if' if="{{selected == 'call-service'}}">
<partial-dev-call-service main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-dev-call-service>
</template>-->
<div main>
<p>Selected: <span>[[selected]]</span></p>
<p>Narrow: <span>[[narrow]]</span></p>
</div>
</paper-drawer-panel>
</template>
@ -235,11 +231,6 @@
type: Boolean,
value: false,
},
hasGroupStates: {
type: Boolean,
computed: '',
}
},
listeners: {

View File

@ -1,28 +1,51 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-scroll-header-panel/core-scroll-header-panel.html">
<link rel="import" href="../bower_components/core-toolbar/core-toolbar.html">
<link rel="import" href="../bower_components/paper-header-panel/paper-header-panel.html">
<!-- <link rel="import" href="../bower_components/core-scroll-header-panel/core-scroll-header-panel.html"> -->
<link rel="import" href="../bower_components/paper-toolbar/paper-toolbar.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<!-- <link rel="import" href="../bower_components/core-style/core-style.html"> -->
<polymer-element name="partial-base" attributes="narrow togglePanel" noscript>
<dom-module is="partial-base">
<template>
<core-style ref="ha-headers"></core-style>
<core-scroll-header-panel fit fixed="{{!narrow}}">
<core-toolbar>
<paper-icon-button
id="navicon" icon="menu" hidden?="{{!narrow}}"
on-click="{{togglePanel}}"></paper-icon-button>
<div flex>
<paper-header-panel class='fit'>
<paper-toolbar>
<paper-icon-button icon="menu" hidden$="[[!narrow]]"
on-click="togglePanel"></paper-icon-button>
<div class='flex'>
<content select="[header-title]"></content>
</div>
<content select="[header-buttons]"></content>
</core-toolbar>
</paper-toolbar>
<content></content>
</core-scroll-header-panel>
</paper-header-panel>
</template>
</polymer>
</dom-module>
<script>
(function() {
Polymer({
is: 'partial-base',
properties: {
narrow: {
type: Boolean,
value: false,
},
togglePanel: {
type: Function,
value: function() {},
},
},
});
})();
</script>

View File

@ -1,16 +1,13 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-icon/core-icon.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="../bower_components/iron-icon/iron-icon.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="./partial-base.html">
<link rel="import" href="../components/state-cards.html">
<polymer-element name="partial-states" attributes="narrow togglePanel filter">
<template>
<core-style ref="ha-animations"></core-style>
<dom-module is="partial-states" attributes="narrow togglePanel filter">
<style>
.listening {
position: absolute;
@ -36,23 +33,27 @@
}
</style>
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
<template>
<!-- <core-style ref="ha-animations"></core-style> -->
<partial-base narrow="[[narrow]]" togglePanel="[[togglePanel]]">
<span header-title>{{headerTitle}}</span>
<span header-buttons>
<paper-icon-button icon="refresh" class="{{isFetching && 'ha-spin'}}"
on-click="{{handleRefreshClick}}" hidden?="{{isStreaming}}"></paper-icon-button>
<paper-icon-button icon="{{isListening ? 'av:mic-off' : 'av:mic' }}" hidden?={{!canListen}}
on-click="{{handleListenClick}}"></paper-icon-button>
<paper-icon-button icon="refresh" class$="[[computeRefreshButtonClass(isFetching)]]"
on-click="handleRefresh" hidden$="[[isStreaming]]"></paper-icon-button>
<paper-icon-button icon="[[listenButtonIcon]]" hidden$={{!canListen}}
on-click="handleListenClick"></paper-icon-button>
</span>
<div class='listening' hidden?="{{!isListening && !isTransmitting}}" on-click={{handleListenClick}}>
<core-icon icon="av:hearing"></core-icon> {{finalTranscript}}
<span class='interimTranscript'>{{interimTranscript}}</span>
<paper-spinner active?="{{isTransmitting}}"></paper-spinner>
<div class='listening' hidden$="[[!showListenInterface]]"
on-click="handleListenClick">
<iron-icon icon="av:hearing"></iron-icon> <span>{{finalTranscript}}</span>
<span class='interimTranscript'>[[interimTranscript]]</span>
<paper-spinner active$="[[isTransmitting]]" alt="Sending voice command to Home Assistant"></paper-spinner>
</div>
<state-cards states="{{states}}">
<state-cards states="[[states]]">
<h3>Hi there!</h3>
<p>
It looks like we have nothing to show you right now. It could be that we have not yet discovered all your devices but it is more likely that you have not configured Home Assistant yet.
@ -62,39 +63,102 @@
</p>
</state-cards>
</partial-base>
</template>
</template>
</dom-module>
<script>
(function(){
var storeListenerMixIn = window.hass.storeListenerMixIn;
var syncActions = window.hass.syncActions;
var voiceActions = window.hass.voiceActions;
var stateStore = window.hass.stateStore;
var uiConstants = window.hass.uiConstants;
Polymer(Polymer.mixin({
headerTitle: "States",
states: [],
isFetching: false,
isStreaming: false,
Polymer({
is: 'partial-states',
canListen: false,
voiceSupported: false,
hasConversationComponent: false,
isListening: false,
isTransmittingVoice: false,
interimTranscript: '',
finalTranscript: '',
behaviors: [StoreListenerBehavior],
ready: function() {
this.voiceSupported = voiceActions.isSupported();
properties: {
/**
* Title to show in the header
*/
headerTitle: {
type: String,
value: 'States',
},
attached: function() {
this.listenToStores(true);
/**
* If header is to be shown in narrow mode.
*/
narrow: {
type: Boolean,
value: false,
},
detached: function() {
this.stopListeningToStores();
/**
* Function to toggle panel visibility.
*/
togglePanel: {
type: Function,
value: function() {},
},
filter: {
type: String,
value: null,
observer: 'filterChanged',
},
voiceSupported: {
type: Boolean,
value: voiceActions.isSupported(),
},
isFetching: {
type: Boolean,
value: false,
},
isStreaming: {
type: Boolean,
value: false,
},
canListen: {
type: Boolean,
value: false,
},
isListening: {
type: Boolean,
value: false,
},
isTransmitting: {
type: Boolean,
value: false,
},
interimTranscript: {
type: String,
value: '',
},
finalTranscript: {
type: String,
value: '',
},
listenButtonIcon: {
type: String,
computed: 'computeListenButtonIcon(isListening)'
},
showListenInterface: {
type: Boolean,
computed: 'computeShowListenInterface(isListening,isTransmitting)'
}
},
componentStoreChanged: function(componentStore) {
@ -145,10 +209,10 @@
}
this.states = states.toArray().filter(
function (el) {return !el.attributes.hidden});
function (el) {return !el.attributes.hidden;});
},
handleRefreshClick: function() {
handleRefresh: function() {
syncActions.fetchAll();
},
@ -159,7 +223,20 @@
voiceActions.listen();
}
},
}, storeListenerMixIn));
computeListenButtonIcon: function(isListening) {
return isListening ? 'av:mic-off' : 'av:mic';
},
computeShowListenInterface: function(isListening,isTransmitting) {
return isListening || isTransmitting;
},
computeRefreshButtonClass: function(isFetching) {
if (isFetching) {
return 'ha-spin';
}
},
});
})();
</script>
</polymer>

View File

@ -1,4 +1,3 @@
<link rel="import" href="../bower_components/iron-icon/iron-icon.html">
<link rel="import" href="../bower_components/iron-iconset-svg/iron-iconset-svg.html">
<link rel="import" href="../bower_components/iron-icons/iron-icons.html">
@ -7,18 +6,17 @@
<link rel="import" href="../bower_components/iron-icons/hardware-icons.html">
<link rel="import" href="../bower_components/iron-icons/av-icons.html">
<iron-iconset-svg name="homeassistant-100" iconSize="100">
<iron-iconset-svg name="homeassistant-100" size="100">
<svg><defs>
<g id="thermostat">
<!--
Thermostat icon created by Scott Lewis from the Noun Project
Licensed under CC BY 3.0 - http://creativecommons.org/licenses/by/3.0/us/
-->
<path d="M66.861,60.105V17.453c0-9.06-7.347-16.405-16.408-16.405c-9.06,0-16.404,7.345-16.404,16.405v42.711 c-4.04,4.14-6.533,9.795-6.533,16.035c0,12.684,10.283,22.967,22.967,22.967c12.682,0,22.964-10.283,22.964-22.967 C73.447,69.933,70.933,64.254,66.861,60.105z M60.331,20.38h-13.21v6.536h6.63v6.539h-6.63v6.713h6.63v6.538h-6.63v6.5h6.63v6.536 h-6.63v7.218c-3.775,1.373-6.471,4.993-6.471,9.24h-6.626c0-5.396,2.598-10.182,6.61-13.185V17.446c0-0.038,0.004-0.075,0.004-0.111 l-0.004-0.007c0-5.437,4.411-9.846,9.849-9.846c5.438,0,9.848,4.409,9.848,9.846V20.38z"/></g>
<g id="thermostat"><path d="M66.861,60.105V17.453c0-9.06-7.347-16.405-16.408-16.405c-9.06,0-16.404,7.345-16.404,16.405v42.711 c-4.04,4.14-6.533,9.795-6.533,16.035c0,12.684,10.283,22.967,22.967,22.967c12.682,0,22.964-10.283,22.964-22.967 C73.447,69.933,70.933,64.254,66.861,60.105z M60.331,20.38h-13.21v6.536h6.63v6.539h-6.63v6.713h6.63v6.538h-6.63v6.5h6.63v6.536 h-6.63v7.218c-3.775,1.373-6.471,4.993-6.471,9.24h-6.626c0-5.396,2.598-10.182,6.61-13.185V17.446c0-0.038,0.004-0.075,0.004-0.111 l-0.004-0.007c0-5.437,4.411-9.846,9.849-9.846c5.438,0,9.848,4.409,9.848,9.846V20.38z"/></g>
</defs></svg>
</iron-iconset-svg>
<iron-iconset-svg name="homeassistant-24" iconSize="24">
<iron-iconset-svg name="homeassistant-24" size="24">
<svg><defs>
<!--
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
@ -29,7 +27,6 @@
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<g id="group"><path d="M9 12c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm5-3c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2zm-2-7c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></g>
</defs></svg>
</iron-iconset-svg>

View File

@ -44,7 +44,7 @@
window.hass.uiActions = {
showMoreInfoDialog: function(entityId) {
dispatcher.dispatch({
actionType: this.ACTION_SHOW_DIALOG_MORE_INFO,
actionType: window.hass.uiConstants.ACTION_SHOW_DIALOG_MORE_INFO,
entityId: entityId,
});
},

View File

@ -1,7 +1,6 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<style is="custom-style">
* {
--dark-primary-color: #0288D1;
--default-primary-color: #03A9F4;
@ -19,6 +18,31 @@
--paper-toggle-button-checked-bar-color: #039be5;
}
@-webkit-keyframes ha-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes ha-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
body /deep/ .ha-spin {
-webkit-animation: ha-spin 2s infinite linear;
animation: ha-spin 2s infinite linear;
}
</style>
<core-style id='ha-main'>