├── .editorconfig
├── .gitignore
├── .travis.yml
├── dist
├── images
│ ├── error.png
│ ├── icon-128.png
│ ├── icon-16.png
│ ├── icon-48.png
│ ├── icon.png
│ ├── loader.png
│ └── rays.png
├── manifest.json
├── options.html
├── popup.html
├── scripts
│ └── options.js
└── styles
│ └── options.css
├── package.json
├── readme.md
└── src
├── data
└── budget.json
├── scripts
├── app.js
├── failure.js
├── loader.js
├── popup.js
└── result.js
└── styles
├── popup.scss
├── result.scss
└── status.scss
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # http://editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | indent_style = space
9 | indent_size = 2
10 | end_of_line = lf
11 | charset = utf-8
12 | trim_trailing_whitespace = true
13 | insert_final_newline = true
14 |
15 | [*.md]
16 | trim_trailing_whitespace = false
17 |
18 | [package.json]
19 | indent_size = 2
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist/scripts/popup.js
3 | dist/styles/popup.css
4 | *.xpi
5 | *.zip
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: "5"
3 | script: npm start
4 |
--------------------------------------------------------------------------------
/dist/images/error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenorocha/browser-calories/706ae5d91dfc16f7b16bdf9f19e0f0c10a13dd0c/dist/images/error.png
--------------------------------------------------------------------------------
/dist/images/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenorocha/browser-calories/706ae5d91dfc16f7b16bdf9f19e0f0c10a13dd0c/dist/images/icon-128.png
--------------------------------------------------------------------------------
/dist/images/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenorocha/browser-calories/706ae5d91dfc16f7b16bdf9f19e0f0c10a13dd0c/dist/images/icon-16.png
--------------------------------------------------------------------------------
/dist/images/icon-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenorocha/browser-calories/706ae5d91dfc16f7b16bdf9f19e0f0c10a13dd0c/dist/images/icon-48.png
--------------------------------------------------------------------------------
/dist/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenorocha/browser-calories/706ae5d91dfc16f7b16bdf9f19e0f0c10a13dd0c/dist/images/icon.png
--------------------------------------------------------------------------------
/dist/images/loader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenorocha/browser-calories/706ae5d91dfc16f7b16bdf9f19e0f0c10a13dd0c/dist/images/loader.png
--------------------------------------------------------------------------------
/dist/images/rays.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenorocha/browser-calories/706ae5d91dfc16f7b16bdf9f19e0f0c10a13dd0c/dist/images/rays.png
--------------------------------------------------------------------------------
/dist/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Browser Calories",
4 | "description": "The easiest way to measure your performance budget",
5 | "homepage_url": "https://github.com/zenorocha/browser-calories",
6 | "version": "1.3.2",
7 | "applications": {
8 | "gecko": {
9 | "id": "calories@browserdiet.com",
10 | "strict_min_version": "45.0"
11 | }
12 | },
13 | "browser_action": {
14 | "default_popup": "popup.html",
15 | "default_icon": "images/icon-48.png"
16 | },
17 | "icons": {
18 | "16" : "images/icon-16.png",
19 | "48" : "images/icon-48.png",
20 | "128": "images/icon-128.png"
21 | },
22 | "permissions": [
23 | "activeTab",
24 | "storage",
25 | "https://www.googleapis.com/"
26 | ],
27 | "options_ui": {
28 | "page": "options.html",
29 | "chrome_style": true
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/dist/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Browser Calories
6 |
7 |
8 |
9 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/dist/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Browser Calories
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/dist/scripts/options.js:
--------------------------------------------------------------------------------
1 | document.addEventListener('DOMContentLoaded', restoreBudget);
2 |
3 | // Firefox has no sync yet.
4 | // Cue: https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
5 | var chromeStorage = chrome.storage.sync || chrome.storage.local;
6 |
7 | var defaultBudget = {
8 | "html" : 62000,
9 | "image" : 1001000,
10 | "css" : 67000,
11 | "js" : 357000,
12 | "other" : 189000,
13 | "total" : 1678000
14 | };
15 |
16 | function restoreBudget() {
17 | var form = document.querySelector('form');
18 | var reset = document.querySelector('#reset');
19 |
20 | form.addEventListener('submit', saveBudget);
21 | reset.addEventListener('click', resetBudget);
22 |
23 | chromeStorage.get(defaultBudget, function(data) {
24 | form.html.value = data.html;
25 | form.image.value = data.image;
26 | form.css.value = data.css;
27 | form.js.value = data.js;
28 | form.other.value = data.other;
29 | });
30 | }
31 |
32 | function saveBudget(e) {
33 | e.preventDefault();
34 |
35 | var budget = {
36 | html : parseInt(e.target.html.value, 10),
37 | image : parseInt(e.target.image.value, 10),
38 | css : parseInt(e.target.css.value, 10),
39 | js : parseInt(e.target.js.value, 10),
40 | other : parseInt(e.target.other.value, 10)
41 | };
42 |
43 | budget.total = budget.html + budget.image + budget.css + budget.js + budget.other;
44 |
45 | chromeStorage.set(budget, function() {
46 | var status = document.querySelector('.status');
47 | status.style.display = 'inline-block';
48 |
49 | setTimeout(function() {
50 | status.style.display = 'none';
51 | }, 750);
52 | });
53 | }
54 |
55 | function resetBudget() {
56 | var form = document.querySelector('form');
57 |
58 | form.html.value = defaultBudget.html;
59 | form.image.value = defaultBudget.image;
60 | form.css.value = defaultBudget.css;
61 | form.js.value = defaultBudget.js;
62 | form.other.value = defaultBudget.other;
63 | }
64 |
--------------------------------------------------------------------------------
/dist/styles/options.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Helvetica Neue", "Lucida Grande", sans-serif;
3 | }
4 |
5 | .description {
6 | color: grey;
7 | font-size: 13px;
8 | font-style: italic;
9 | }
10 |
11 | .input-container {
12 | clear: both;
13 | display: block;
14 | margin-bottom: 10px;
15 | }
16 |
17 | .input-container label {
18 | display: inline-block;
19 | font-size: 12px;
20 | width: 65px;
21 | }
22 |
23 | .input-container input[type='number'] {
24 | width: 100px;
25 | }
26 |
27 | .button {
28 | cursor: pointer;
29 | margin-top: 10px;
30 | margin-right: 5px;
31 | }
32 |
33 | .status {
34 | display: none;
35 | font-size: 12px;
36 | font-style: italic;
37 | margin-left: 5px;
38 | }
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "browser-calories",
3 | "version": "1.3.2",
4 | "description": "The easiest way to measure your performance budget",
5 | "main": "src/scripts/popup.js",
6 | "repository": "zenorocha/browser-calories",
7 | "license": "MIT",
8 | "dependencies": {
9 | "byte-size": "2.0.0",
10 | "metal-component": "1.0.0-rc.16",
11 | "metal-jsx": "1.0.0-rc.12"
12 | },
13 | "devDependencies": {
14 | "babel-preset-es2015": "6.9.0",
15 | "babel-preset-metal-jsx": "0.0.3",
16 | "babelify": "7.3.0",
17 | "browserify": "13.0.1",
18 | "envify": "3.4.0",
19 | "node-sass": "3.7.0",
20 | "onchange": "2.4.0",
21 | "parallelshell": "2.0.0",
22 | "watchify": "3.7.0"
23 | },
24 | "scripts": {
25 | "start": "npm run build",
26 | "build": "npm run build:scripts && npm run build:styles",
27 | "build:scripts": "browserify src/scripts/popup.js -t [ babelify --presets [ es2015 metal-jsx ] ] -t [ envify ] -o dist/scripts/popup.js",
28 | "build:styles": "node-sass src/styles/popup.scss dist/styles/popup.css -q",
29 | "watch": "parallelshell 'npm run watch:scripts' 'npm run watch:styles'",
30 | "watch:scripts": "onchange 'src/scripts/*.js' -- npm run build:scripts",
31 | "watch:styles": "onchange 'src/styles/*.scss' -- npm run build:styles",
32 | "package": "npm run package:blink && npm run package:gecko",
33 | "package:blink": "cd dist && zip -r ../browser-calories.zip * && cd ..",
34 | "package:gecko": "cd dist && zip -r ../browser-calories.xpi * && cd .."
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Browser Calories
2 |
3 | [](https://travis-ci.org/zenorocha/browser-calories)
4 | 
5 |
6 | > The easiest way to measure your performance budget.
7 |
8 | [](https://browserdiet.com/calories/)
9 |
10 | ## Install
11 |
12 | This browser extension available for:
13 |
14 | |
|
|
|
15 | |:---:|:---:|:---:|
16 | | [Chrome](https://chrome.google.com/webstore/detail/browser-calories/pdkibgfjegigkoaleelbkdpkgceljfco) | [Firefox](https://addons.mozilla.org/en-US/firefox/addon/browser-calories/) | [Opera](https://addons.opera.com/en/extensions/details/browser-calories) |
17 |
18 | ## Setup
19 |
20 | Install dependencies:
21 |
22 | ```
23 | npm install
24 | ```
25 |
26 | Compile scripts and styles:
27 |
28 | ```
29 | npm start
30 | ```
31 |
32 | ## Testing
33 |
34 | ###### Chrome
35 |
36 | 1. Navigate to `chrome://extensions`
37 |
38 | 2. Click on `Load unpacked extension...`
39 |
40 | 3. Select the `dist` folder
41 |
42 | ###### Firefox
43 |
44 | 1. Navigate to `about:debugging`
45 |
46 | 2. Click on `Load temporary Add-on`
47 |
48 | 3. Select the `manifest.json` inside the `dist` folder
49 |
50 | ###### Opera
51 |
52 | 1. Navigate to `extensions`
53 |
54 | 2. Click on `Developer Mode`
55 |
56 | 3. Click on `Load unpacked extension...`
57 |
58 | 4. Select the `dist` folder
59 |
60 | ## Credits
61 |
62 | * Illustrations by [Scott Johnson](https://twitter.com/scottjohnson)
63 | * CSS-based Nutrition Facts table by [Chris Coyier](https://twitter.com/chriscoyier)
64 |
65 | ## License
66 |
67 | [MIT License](http://zenorocha.mit-license.org/) © Zeno Rocha
68 |
--------------------------------------------------------------------------------
/src/data/budget.json:
--------------------------------------------------------------------------------
1 | {
2 | "html" : 62000,
3 | "image" : 1001000,
4 | "css" : 67000,
5 | "js" : 357000,
6 | "other" : 189000,
7 | "total" : 1678000
8 | }
9 |
--------------------------------------------------------------------------------
/src/scripts/app.js:
--------------------------------------------------------------------------------
1 | import JSXComponent from 'metal-jsx';
2 | import Loader from './loader';
3 | import Result from './result';
4 | import Failure from './failure';
5 | import defaultBudget from '../data/budget';
6 |
7 | class App extends JSXComponent {
8 | attached() {
9 | this.getURL();
10 | this.getBudget();
11 | }
12 |
13 | getURL() {
14 | chrome.tabs.query({
15 | active: true,
16 | currentWindow: true
17 | }, (tabs) => {
18 | this.setState({
19 | url: tabs[0].url
20 | }, this.fetchURL);
21 | });
22 | }
23 |
24 | getBudget() {
25 | // Firefox has no sync yet.
26 | // Cue: https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
27 | let chromeStorage = chrome.storage.sync || chrome.storage.local;
28 |
29 | chromeStorage.get(defaultBudget, (data) => {
30 | this.setState({
31 | budget: data
32 | });
33 | });
34 | }
35 |
36 | fetchURL() {
37 | let endpoint = 'https://www.googleapis.com/pagespeedonline/v2/runPagespeed?url=' + encodeURIComponent(this.url);
38 |
39 | if (process.env.API_KEY) {
40 | endpoint += `&key=${process.env.API_KEY}`;
41 | }
42 |
43 | fetch(endpoint)
44 | .then((response) => {
45 | return response.json();
46 | })
47 | .then((response) => {
48 | if (response.error) {
49 | this.setState({
50 | error: response.error
51 | });
52 | } else {
53 | this.setState({
54 | success: response.pageStats
55 | });
56 | }
57 | });
58 | }
59 |
60 | render() {
61 | if (this.success && this.budget) {
62 | return ;
63 | }
64 | else if (this.error) {
65 | return ;
66 | }
67 | else {
68 | return ;
69 | }
70 | }
71 | }
72 |
73 | App.STATE = {
74 | url: {},
75 | budget: {},
76 | error: {},
77 | success: {}
78 | };
79 |
80 | export default App;
81 |
--------------------------------------------------------------------------------
/src/scripts/failure.js:
--------------------------------------------------------------------------------
1 | import JSXComponent from 'metal-jsx';
2 |
3 | class Failure extends JSXComponent {
4 | render() {
5 | let title = `Error ${this.config.error.code}`;
6 |
7 | if (this.config.error.code === 400) {
8 | title = 'URL can\'t be reached';
9 | }
10 |
11 | return (
12 |
13 |

14 |

15 |
19 |
20 | )
21 | }
22 | }
23 |
24 | export default Failure;
25 |
--------------------------------------------------------------------------------
/src/scripts/loader.js:
--------------------------------------------------------------------------------
1 | import JSXComponent from 'metal-jsx';
2 |
3 | class Loader extends JSXComponent {
4 | render() {
5 | return (
6 |
7 |

8 |

9 |
10 |
Measuring
11 |
{this.config.url}
12 |
13 |
14 | )
15 | }
16 | }
17 |
18 | export default Loader;
19 |
--------------------------------------------------------------------------------
/src/scripts/popup.js:
--------------------------------------------------------------------------------
1 | import App from './app';
2 |
3 | new App({}, document.querySelector('#wrapper'));
4 |
--------------------------------------------------------------------------------
/src/scripts/result.js:
--------------------------------------------------------------------------------
1 | import JSXComponent from 'metal-jsx';
2 | import bytes from 'byte-size';
3 |
4 | class Result extends JSXComponent {
5 | toInt(data) {
6 | var result = {
7 | html : parseInt(data.htmlResponseBytes, 10) || 0,
8 | css : parseInt(data.cssResponseBytes, 10) || 0,
9 | image : parseInt(data.imageResponseBytes, 10) || 0,
10 | js : parseInt(data.javascriptResponseBytes, 10) || 0,
11 | other : parseInt(data.otherResponseBytes, 10) || 0
12 | };
13 |
14 | result.total = result.html + result.image + result.css + result.js + result.other;
15 |
16 | return result;
17 | }
18 |
19 | toBytes(data) {
20 | var obj = {};
21 |
22 | for (var prop in data) {
23 | if (data.hasOwnProperty(prop)) {
24 | obj[prop] = bytes(data[prop], { precision: 1 });
25 | }
26 | }
27 |
28 | return obj;
29 | }
30 |
31 | toPercentage(a, b) {
32 | var obj = {};
33 |
34 | for (var prop in a) {
35 | if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop)) {
36 | if (a[prop] === 0 || b[prop] === 0 ) {
37 | obj[prop] = 0;
38 | } else {
39 | obj[prop] = Math.round((a[prop] / b[prop]) * 100);
40 | }
41 | }
42 | }
43 |
44 | return obj;
45 | }
46 |
47 | isPositive(number) {
48 | let className = 'facts-percentage ';
49 |
50 | if (number >= 100) {
51 | className += 'bad';
52 | } else {
53 | className += 'good';
54 | }
55 |
56 | return className;
57 | }
58 |
59 | openSettings() {
60 | // Firefox has not completed the implementation of runtime
61 | // Cue: https://bugzilla.mozilla.org/show_bug.cgi?id=1213473
62 | let chromeRuntime = chrome.runtime || runtime;
63 |
64 | chromeRuntime.openOptionsPage();
65 | }
66 |
67 | render() {
68 | let cleanUrl = this.config.url.replace(/^http(s)?\:\/\/(www.)?/i, "").replace(/\/$/, "");
69 | let siteStats = this.toInt(this.config.success);
70 | let siteBytes = this.toBytes(siteStats);
71 | let budgetBytes = this.toBytes(this.config.budget);
72 | let dailyPercentage = this.toPercentage(siteStats, this.config.budget);
73 |
74 | return (
75 |
76 |
83 |
84 |
85 |
86 |
87 | Amount Per Serving
88 | |
89 |
90 | % Per Load *
91 | |
92 |
93 |
94 |
95 |
96 |
97 | HTML
98 | {siteBytes.html}
99 | |
100 |
101 | {dailyPercentage.html}%
102 | |
103 |
104 |
105 |
106 | Images
107 | {siteBytes.image}
108 | |
109 |
110 | {dailyPercentage.image}%
111 | |
112 |
113 |
114 |
115 | CSS
116 | {siteBytes.css}
117 | |
118 |
119 | {dailyPercentage.css}%
120 | |
121 |
122 |
123 |
124 | JavaScript
125 | {siteBytes.js}
126 | |
127 |
128 | {dailyPercentage.js}%
129 | |
130 |
131 |
132 |
133 | Other
134 | {siteBytes.other}
135 | |
136 |
137 | {dailyPercentage.other}%
138 | |
139 |
140 |
141 |
142 | Total Size
143 | {siteBytes.total}
144 | |
145 |
146 | {dailyPercentage.total}%
147 | |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | HTML |
156 | Less than |
157 | {budgetBytes.html} |
158 |
159 |
160 | Images |
161 | Less than |
162 | {budgetBytes.image} |
163 |
164 |
165 | CSS |
166 | Less than |
167 | {budgetBytes.css} |
168 |
169 |
170 | JavaScript |
171 | Less than |
172 | {budgetBytes.js} |
173 |
174 |
175 | Other |
176 | Less than |
177 | {budgetBytes.other} |
178 |
179 |
180 | Total Size |
181 | Less than |
182 | {budgetBytes.total} |
183 |
184 |
185 |
186 |
190 |
191 | )
192 | }
193 | }
194 |
195 | export default Result;
196 |
--------------------------------------------------------------------------------
/src/styles/popup.scss:
--------------------------------------------------------------------------------
1 | body {
2 | background: #ffde00;
3 | font-family: sans-serif;
4 | font-size: small;
5 | line-height: 1.4;
6 | margin: 0;
7 | overflow-x: hidden;
8 | }
9 |
10 | a {
11 | color: black;
12 | cursor: pointer;
13 | text-decoration: underline;
14 | }
15 |
16 | p {
17 | margin: 0;
18 | }
19 |
20 | #wrapper {
21 | width: 325px;
22 | }
23 |
24 | @import 'result.scss';
25 | @import 'status.scss';
26 |
--------------------------------------------------------------------------------
/src/styles/result.scss:
--------------------------------------------------------------------------------
1 | .facts {
2 | background: white;
3 | border: 1px solid black;
4 | float: left;
5 | padding: 5px;
6 |
7 | table {
8 | border-collapse: collapse;
9 | }
10 | }
11 |
12 | .facts-header {
13 | padding: 0 0 0.25rem 0;
14 | margin: 0 0 0.5rem 0;
15 |
16 | p {
17 | margin: 0;
18 | }
19 | }
20 |
21 | .facts-title {
22 | font-weight: bold;
23 | font-size: 2rem;
24 | margin: 0;
25 | }
26 |
27 | .facts-url {
28 | background: #e5e5e5;
29 | border-radius: 3px;
30 | color: #797979;
31 | display: inline-block;
32 | padding: 1px 6px;
33 | max-width: 100px;
34 | text-decoration: none;
35 | text-overflow: ellipsis;
36 | white-space: nowrap;
37 | overflow: hidden;
38 | position: relative;
39 | top: 5px;
40 | left: 10px;
41 | }
42 |
43 | .facts-table {
44 | width: 100%;
45 |
46 | thead tr {
47 | th, td {
48 | border: 0;
49 | }
50 | }
51 |
52 | th, td {
53 | font-weight: normal;
54 | text-align: left;
55 | padding: 0.25rem 0;
56 | border-top: 1px solid black;
57 | white-space: nowrap;
58 | }
59 |
60 | td:last-child {
61 | text-align: right;
62 | }
63 | }
64 |
65 | .facts-table-main {
66 | border-top: 10px solid black;
67 | border-bottom: 10px solid black;
68 | margin: 0 0 0.7rem 0;
69 |
70 | tr:last-child {
71 | th, td {
72 | border-top-width: 5px;
73 | }
74 | }
75 |
76 | .facts-percentage {
77 | border-radius: 3px;
78 | color: white;
79 | display: inline-block;
80 | min-width: 34px;
81 | padding: 0px 4px;
82 | text-align: center;
83 |
84 | &.good {
85 | background: #6bc500;
86 | }
87 |
88 | &.bad {
89 | background: #e60014;
90 | }
91 | }
92 | }
93 |
94 | .facts-table-small {
95 | color: #797979;
96 | margin: 0.3rem 0;
97 |
98 | tr {
99 | &:first-child {
100 | margin-top: 0.3rem;
101 | }
102 |
103 | &:last-child {
104 | margin-bottom: 0.3rem;
105 | }
106 | }
107 |
108 | td:last-child {
109 | text-align: left;
110 | }
111 |
112 | th, td {
113 | border: 0;
114 | padding: 0;
115 | }
116 | }
117 |
118 | .facts-table-small-header {
119 | border-bottom: 1px solid #999;
120 | color: #797979;
121 | margin-top: 0.3rem;
122 | padding-bottom: 0.3rem;
123 | }
124 |
125 | .facts-table-small-footer {
126 | border-top: 1px solid #999;
127 | margin-top: 0.3rem;
128 | padding-top: 0.3rem;
129 | text-align: center;
130 | }
131 |
132 | .small-info {
133 | font-size: 0.7rem;
134 | }
135 |
--------------------------------------------------------------------------------
/src/styles/status.scss:
--------------------------------------------------------------------------------
1 | .status {
2 | overflow: hidden;
3 | position: relative;
4 | }
5 |
6 | .status-geek {
7 | display: block;
8 | margin: 0 auto;
9 | padding: 20px;
10 | height: 315px;
11 | }
12 |
13 | .status-rays {
14 | -webkit-animation: 20s rays infinite linear;
15 | animation: 20s rays infinite linear;
16 | position: absolute;
17 | left: -100px;
18 | top: -100px;
19 | z-index: -1;
20 | }
21 |
22 | .status-msg {
23 | background: #e60014;
24 | color: #ffde00;
25 | padding: 10px;
26 | text-align: center;
27 |
28 | p {
29 | overflow: hidden;
30 | text-overflow: ellipsis;
31 | white-space: nowrap;
32 |
33 | &:first-child {
34 | font-size: 18px;
35 | }
36 | }
37 |
38 | a {
39 | color: #ffde00;
40 | border-bottom: 1px solid #ffde00;
41 | text-decoration: none;
42 | }
43 | }
44 |
45 | @-webkit-keyframes rays {
46 | to {
47 | -webkit-transform: rotate(360deg);
48 | transform: rotate(360deg)
49 | }
50 | }
51 |
52 | @keyframes rays {
53 | to {
54 | -webkit-transform: rotate(360deg);
55 | transform: rotate(360deg)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------