+
+
+
+
diff --git a/extension/app/pages/styles/report.css b/extension/app/pages/styles/report.css
new file mode 100644
index 000000000000..8be59260d890
--- /dev/null
+++ b/extension/app/pages/styles/report.css
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+* {
+ box-sizing: border-box;
+}
+
+html, body {
+ padding: 0;
+ margin: 0;
+ background: #FAFAFA;
+ color: #444;
+ font-family: Arial, sans-serif;
+}
+
+body {
+ min-width: 600px;
+ padding: 0 16px;
+}
+
+a {
+ color: #57A0A8;
+}
+
+.spinner-container {
+ width: 100%;
+ height: 100%;
+ position: fixed;
+ top: 0;
+ left: 0;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ pointer-events: none;
+}
+
+.spinner {
+ width: 34px;
+ height: 34px;
+ border-radius: 50%;
+ background: url(../images/spinner.png) center center no-repeat;
+ background-size: 34px 34px;
+ will-change: transform;
+}
diff --git a/extension/app/popup.html b/extension/app/popup.html
index acaee42d1a36..74a754fd77a9 100644
--- a/extension/app/popup.html
+++ b/extension/app/popup.html
@@ -1,3 +1,20 @@
+
@@ -15,7 +32,7 @@
Lighthouse
...
Beta
-
+
diff --git a/extension/app/scripts.babel/app.js b/extension/app/scripts.babel/app.js
index 648281b523bd..14b4a6e35ae7 100644
--- a/extension/app/scripts.babel/app.js
+++ b/extension/app/scripts.babel/app.js
@@ -19,25 +19,27 @@ document.addEventListener('DOMContentLoaded', _ => {
const background = chrome.extension.getBackgroundPage();
const siteNameEl = window.document.querySelector('header h2');
const resultsEl = document.body.querySelector('.results');
- const reloadPage = document.body.querySelector('.reload-all');
+ const generateFullReportEl = document.body.querySelector('.generate-full-report');
background.runAudits({
flags: {
mobile: false,
loadPage: false
}
- }).then(ret => {
- resultsEl.innerHTML = ret;
+ })
+ .then(results => {
+ resultsEl.innerHTML = background.createResultsHTML(results);
});
- reloadPage.addEventListener('click', () => {
+ generateFullReportEl.addEventListener('click', () => {
background.runAudits({
flags: {
mobile: true,
loadPage: true
}
- }).then(ret => {
- resultsEl.innerHTML = ret;
+ })
+ .then(results => {
+ background.createPageAndPopulate(results);
});
});
diff --git a/extension/app/scripts.babel/background.js b/extension/app/scripts.babel/background.js
index 1e4d53cdfe4f..9103d50751c7 100644
--- a/extension/app/scripts.babel/background.js
+++ b/extension/app/scripts.babel/background.js
@@ -21,6 +21,18 @@ const ExtensionProtocol = require('../../../helpers/extension/driver.js');
const runner = require('../../../runner');
const NO_SCORE_PROVIDED = '-1';
+window.createPageAndPopulate = function(results) {
+ const tabURL = chrome.extension.getURL('/pages/report.html');
+ chrome.tabs.create({url: tabURL}, tab => {
+ // Have a timeout here so that the receiving side has time to load
+ // and register an event listener for onMessage. Otherwise the
+ // message sent with the results will be lost.
+ setTimeout(_ => {
+ chrome.tabs.sendMessage(tab.id, results);
+ }, 1000);
+ });
+};
+
window.runAudits = function(options) {
const driver = new ExtensionProtocol();
@@ -29,7 +41,6 @@ window.runAudits = function(options) {
// Add in the URL to the options.
return runner(driver, Object.assign({}, options, {url}));
})
- .then(results => createResultsHTML(results))
.catch(returnError);
};
@@ -46,10 +57,10 @@ function escapeHTML(str) {
.replace(/`/g, '`');
}
-function createResultsHTML(results) {
+window.createResultsHTML = function(results) {
let resultsHTML = '';
- results.forEach(item => {
+ results.aggregations.forEach(item => {
const score = (item.score.overall * 100).toFixed(0);
const groupHasErrors = (score < 100);
const groupClass = 'group ' +
@@ -82,7 +93,7 @@ function createResultsHTML(results) {
});
return resultsHTML;
-}
+};
chrome.runtime.onInstalled.addListener(details => {
console.log('previousVersion', details.previousVersion);
diff --git a/extension/app/scripts.babel/report.js b/extension/app/scripts.babel/report.js
new file mode 100644
index 000000000000..6a3d6762adb7
--- /dev/null
+++ b/extension/app/scripts.babel/report.js
@@ -0,0 +1,56 @@
+/**
+ * @license
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const Report = require('../../../report/report.js');
+
+class ReportLoader {
+ constructor() {
+ this.spinnerEl = document.querySelector('.js-spinner');
+ this.startSpinner();
+ }
+
+ startSpinner() {
+ this.spinnerEl.classList.remove('spinner--hidden');
+ this.spinnerAnimation = this.spinnerEl.animate([
+ {transform: 'rotate(0deg)'},
+ {transform: 'rotate(359deg)'}
+ ], {
+ duration: 1000,
+ iterations: Infinity
+ });
+ }
+
+ stopSpinner() {
+ this.spinnerAnimation.cancel();
+ this.spinnerEl.classList.add('spinner--hidden');
+ }
+
+ write(results) {
+ const report = new Report();
+ report.generateHTML(results).then(html => {
+ this.stopSpinner();
+ document.documentElement.innerHTML = html;
+ });
+ }
+}
+
+const report = new ReportLoader();
+
+if (chrome && chrome.runtime && chrome.runtime.onMessage) {
+ chrome.runtime.onMessage.addListener(data => {
+ report.write(data);
+ });
+}
diff --git a/extension/app/styles/lighthouse.css b/extension/app/styles/lighthouse.css
index 82825df38d5b..237915de5f62 100644
--- a/extension/app/styles/lighthouse.css
+++ b/extension/app/styles/lighthouse.css
@@ -163,7 +163,7 @@ header a {
color: #D0021B;
}
-.reload-all {
+.generate-full-report {
display: block;
font-size: 14px;
padding: 5px;
diff --git a/extension/gulpfile.babel.js b/extension/gulpfile.babel.js
index 9ce0475e3d4a..4ce4940714f2 100644
--- a/extension/gulpfile.babel.js
+++ b/extension/gulpfile.babel.js
@@ -3,6 +3,7 @@ import gulp from 'gulp';
import gulpLoadPlugins from 'gulp-load-plugins';
import del from 'del';
import browserify from 'gulp-browserify';
+import brfs from 'brfs';
import runSequence from 'run-sequence';
var debug = require('gulp-debug');
@@ -84,7 +85,8 @@ gulp.task('babel', () => {
return gulp.src([
'app/scripts.babel/app.js',
'app/scripts.babel/chromereload.js',
- 'app/scripts.babel/background.js'])
+ 'app/scripts.babel/background.js',
+ 'app/scripts.babel/report.js'])
.pipe($.rollup())
.pipe($.babel({
presets: ['es2015']
@@ -93,7 +95,8 @@ gulp.task('babel', () => {
ignore: [
'npmlog',
'chrome-remote-interface'
- ]
+ ],
+ transform: ['brfs']
}))
.pipe(gulp.dest('app/scripts'))
.pipe(gulp.dest('dist/scripts'));
diff --git a/extension/package.json b/extension/package.json
index 37d175bbfb5f..a294adeac937 100644
--- a/extension/package.json
+++ b/extension/package.json
@@ -15,6 +15,7 @@
"eslint-config-google": "^0.4.0",
"gulp": "^3.9.1",
"gulp-babel": "^6.1.2",
+ "gulp-brfs": "^0.1.0",
"gulp-browserify": "^0.5.1",
"gulp-cache": "^0.4.2",
"gulp-chrome-manifest": "0.0.13",
diff --git a/gather-scheduler.js b/gather-scheduler.js
index ac098f0282f6..3c9fbcd29c08 100644
--- a/gather-scheduler.js
+++ b/gather-scheduler.js
@@ -16,6 +16,8 @@
*/
'use strict';
+const fs = require('fs');
+
const log = (typeof process !== 'undefined' && 'version' in process) ?
require('npmlog').log : console.log.bind(console);
@@ -142,7 +144,7 @@ class GatherScheduler {
const hostname = url.match(/^.*?\/\/(.*?)(:?\/|$)/)[1];
const filename = (hostname + '_' + date.toISOString() + '.trace.json')
.replace(/[\/\?<>\\:\*\|":]/g, '-');
- require('fs').writeFileSync(filename, JSON.stringify(tracingData.traceContents, null, 2));
+ fs.writeFileSync(filename, JSON.stringify(tracingData.traceContents, null, 2));
log('info', 'trace file saved to disk', filename);
}
}
diff --git a/package.json b/package.json
index 41852b170757..f3784e000fed 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"chrome-devtools-frontend": "1.0.381789",
"chrome-remote-interface": "^0.11.0",
"devtools-timeline-model": "1.0.19",
+ "handlebars": "^4.0.5",
"meow": "^3.7.0",
"npmlog": "^2.0.3",
"semver": "^5.1.0",
diff --git a/report/report.js b/report/report.js
new file mode 100644
index 000000000000..b51acb32b7fb
--- /dev/null
+++ b/report/report.js
@@ -0,0 +1,111 @@
+/**
+ * @license
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+'use strict';
+
+/* global Intl */
+
+const Handlebars = require('handlebars');
+const fs = require('fs');
+const path = require('path');
+
+class Report {
+
+ constructor() {
+ Handlebars.registerHelper('generated', _ => {
+ const options = {
+ day: 'numeric', month: 'numeric', year: 'numeric',
+ hour: 'numeric', minute: 'numeric', second: 'numeric',
+ timeZoneName: 'short'
+ };
+ const formatter = new Intl.DateTimeFormat('en-US', options);
+ return formatter.format(new Date());
+ });
+
+ Handlebars.registerHelper('generateAnchor', shortName => {
+ return shortName.toLowerCase().replace(/\s/gim, '');
+ });
+
+ Handlebars.registerHelper('getItemValue', value => {
+ if (typeof value === 'boolean') {
+ return value ? 'Yes' : 'No';
+ }
+
+ return value;
+ });
+
+ Handlebars.registerHelper('getItemRating', value => {
+ if (typeof value === 'boolean') {
+ return value ? 'good' : 'poor';
+ }
+
+ let rating = 'poor';
+ if (value > 0.33) {
+ rating = 'average';
+ }
+ if (value > 0.66) {
+ rating = 'good';
+ }
+
+ return rating;
+ });
+
+ Handlebars.registerHelper('convertToPercentage', value => {
+ return Math.floor(value * 100);
+ });
+
+ Handlebars.registerHelper('getItemRawValue', subItem => {
+ let value = '';
+ if (typeof subItem.rawValue !== 'undefined') {
+ let optimalValue = '';
+ if (typeof subItem.optimalValue !== 'undefined') {
+ optimalValue = ` / ${subItem.optimalValue}`;
+ }
+
+ value = ` (${subItem.rawValue}${optimalValue})`;
+ }
+
+ return value;
+ });
+ }
+
+ getReportHTML() {
+ return fs.readFileSync(path.join(__dirname, './templates/report.html'), 'utf8');
+ }
+
+ getReportCSS() {
+ return fs.readFileSync(path.join(__dirname, './styles/report.css'), 'utf8');
+ }
+
+ generateHTML(results) {
+ const totalScore =
+ (results.aggregations.reduce((prev, aggregation) => {
+ return prev + aggregation.score.overall;
+ }, 0) /
+ results.aggregations.length);
+
+ const template = Handlebars.compile(this.getReportHTML());
+ // TODO(bckenny): is this async?
+ return template({
+ url: results.url,
+ totalScore: Math.round(totalScore * 100),
+ css: this.getReportCSS(),
+ aggregations: results.aggregations
+ });
+ }
+}
+
+module.exports = Report;
diff --git a/report/styles/report.css b/report/styles/report.css
new file mode 100644
index 000000000000..db7fe4600dd1
--- /dev/null
+++ b/report/styles/report.css
@@ -0,0 +1,324 @@
+/**
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 100;
+ src: local('Roboto Thin'), local('Roboto-Thin') format('woff');
+}
+@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Roboto'), local('Roboto-Regular') format('woff');
+}
+@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 500;
+ src: local('Roboto Medium'), local('Roboto-Medium') format('woff');
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html, body {
+ padding: 0;
+ margin: 0;
+ background: #FAFAFA;
+ color: #444;
+ font-family: Arial, sans-serif;
+ font-size: 16px;
+}
+
+body {
+ min-width: 600px;
+ padding: 0 16px;
+}
+
+a {
+ color: #57A0A8;
+}
+
+.report {
+ width: 100%;
+ margin: 16px auto 100px auto;
+ border-radius: 4px;
+ max-width: 1280px;
+ background: #FFF;
+ box-shadow: 0px 4px 6px 0px rgba(0,0,0,0.26);
+}
+
+.header {
+ height: 180px;
+ background: #2D3441;
+ display: flex;
+ flex-direction: row;
+ border-radius: 4px 4px 0 0;
+ font-family: 'Roboto', Arial, sans-serif;
+ position: relative;
+}
+
+.header::after {
+ content: '';
+ display: block;
+ width: 90px;
+ height: 90px;
+ position: absolute;
+ top: 0;
+ right: 0;
+ background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODYiIGhlaWdodD0iODYiIHZpZXdCb3g9IjAgMCA4NiA4NiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PHRpdGxlPkJldGE8L3RpdGxlPjxkZWZzPjxwYXRoIGlkPSJiIiBkPSJNLTExLjcwNCAxMy4xNDRIMTI1LjU4djMwSC0xMS43MDN6Ii8+PGZpbHRlciB4PSItNTAlIiB5PSItNTAlIiB3aWR0aD0iMjAwJSIgaGVpZ2h0PSIyMDAlIiBmaWx0ZXJVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giIGlkPSJhIj48ZmVPZmZzZXQgZHk9IjEiIGluPSJTb3VyY2VBbHBoYSIgcmVzdWx0PSJzaGFkb3dPZmZzZXRPdXRlcjEiLz48ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIxIiBpbj0ic2hhZG93T2Zmc2V0T3V0ZXIxIiByZXN1bHQ9InNoYWRvd0JsdXJPdXRlcjEiLz48ZmVDb2xvck1hdHJpeCB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuNSAwIiBpbj0ic2hhZG93Qmx1ck91dGVyMSIvPjwvZmlsdGVyPjx0ZXh0IGlkPSJkIiBmb250LWZhbWlseT0iUm9ib3RvLUJvbGQsIFJvYm90byIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNGRkYiPjx0c3BhbiB4PSI0My41NTYiIHk9IjM3LjU1NiI+QkVUQTwvdHNwYW4+PC90ZXh0PjxmaWx0ZXIgeD0iLTUwJSIgeT0iLTUwJSIgd2lkdGg9IjIwMCUiIGhlaWdodD0iMjAwJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94IiBpZD0iYyI+PGZlT2Zmc2V0IGR5PSIxIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIi8+PGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iLjUiIGluPSJzaGFkb3dPZmZzZXRPdXRlcjEiIHJlc3VsdD0ic2hhZG93Qmx1ck91dGVyMSIvPjxmZUNvbG9yTWF0cml4IHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xNDA5NjQ2NzQgMCIgaW49InNoYWRvd0JsdXJPdXRlcjEiLz48L2ZpbHRlcj48cGF0aCBpZD0iZiIgZD0iTS40IDE2Ljk3MmgxMTl2MjguNEguNHoiLz48ZmlsdGVyIHg9Ii01MCUiIHk9Ii01MCUiIHdpZHRoPSIyMDAlIiBoZWlnaHQ9IjIwMCUiIGZpbHRlclVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgaWQ9ImUiPjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjMuNSIgaW49IlNvdXJjZUFscGhhIiByZXN1bHQ9InNoYWRvd0JsdXJJbm5lcjEiLz48ZmVPZmZzZXQgaW49InNoYWRvd0JsdXJJbm5lcjEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0SW5uZXIxIi8+PGZlQ29tcG9zaXRlIGluPSJzaGFkb3dPZmZzZXRJbm5lcjEiIGluMj0iU291cmNlQWxwaGEiIG9wZXJhdG9yPSJhcml0aG1ldGljIiBrMj0iLTEiIGszPSIxIiByZXN1bHQ9InNoYWRvd0lubmVySW5uZXIxIi8+PGZlQ29sb3JNYXRyaXggdmFsdWVzPSIwIDAgMCAwIDEgMCAwIDAgMCAxIDAgMCAwIDAgMSAwIDAgMCAwLjY4OTUwOTczNyAwIiBpbj0ic2hhZG93SW5uZXJJbm5lcjEiLz48L2ZpbHRlcj48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48ZyBtYXNrPSJ1cmwoI21hc2stMikiIHRyYW5zZm9ybT0icm90YXRlKDQ1IDU1LjQ0IDI0LjUyMykiPjx1c2UgZmlsbD0iIzAwMCIgZmlsdGVyPSJ1cmwoI2EpIiB4bGluazpocmVmPSIjYiIvPjx1c2UgZmlsbD0iI0NGM0EzQyIgeGxpbms6aHJlZj0iI2IiLz48L2c+PGcgbWFzaz0idXJsKCNtYXNrLTIpIiB0cmFuc2Zvcm09InJvdGF0ZSg0NSA1OS41NTYgMjguOTM1KSIgZmlsbD0iI0ZGRiI+PHVzZSBmaWx0ZXI9InVybCgjYykiIHhsaW5rOmhyZWY9IiNkIi8+PHVzZSB4bGluazpocmVmPSIjZCIvPjwvZz48dXNlIGZpbHRlcj0idXJsKCNlKSIgeGxpbms6aHJlZj0iI2YiIG1hc2s9InVybCgjbWFzay0yKSIgdHJhbnNmb3JtPSJyb3RhdGUoNDUgNTguNCAyNy41NSkiIGZpbGw9IiMwMDAiLz48cGF0aCBkPSJNOC41LS41bDg4LjIwNCA4OC4yMDRNOC41LTM5LjVsODguMjA0IDg4LjIwNCIgc3Ryb2tlPSIjRkZGIiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIiBzdHJva2UtZGFzaGFycmF5PSIxLDIiIG9wYWNpdHk9Ii4zODYiIG1hc2s9InVybCgjbWFzay0yKSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTMpIi8+PC9nPjwvc3ZnPg==') top right no-repeat;
+}
+
+.header-titles {
+ height: 100%;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 0 0 0 5%;
+}
+
+.header-titles__main,
+.header-titles__url {
+ margin: 0;
+ -webkit-font-smoothing: antialiased;
+}
+
+.header-titles__main {
+ font-size: 36px;
+ color: #FFF;
+ font-weight: 400;
+}
+
+.header-titles__url {
+ color: #FFF;
+ opacity: 0.65;
+ font-size: 18px;
+ font-weight: 400;
+}
+
+.header__score {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ border-radius: 0 4px 0 0;
+ height: 100%;
+ padding-left: 120px;
+ position: relative;
+ padding: 0 5% 0 0;
+ background: #45535F;
+}
+
+.header__score::before {
+ content: '';
+ display: block;
+ width: 120px;
+ height: 100%;
+ background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzY0IiBoZWlnaHQ9IjE4NCIgdmlld0JveD0iMCAwIDM2NCAxODQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRpdGxlPlBhdGggNDY8L3RpdGxlPjxwYXRoIHN0cm9rZT0iI0ZGRiIgZmlsbD0iIzQ1NTM1RiIgZD0iTTEgMTgzLjMxTDEwNC41NiAxaDI1OC4yMnYxODIuMzF6IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZS1vcGFjaXR5PSIuMTUiLz48L3N2Zz4K');
+ background-position: 0 -1px;
+ position: absolute;
+ left: -120px;
+ top: 0;
+}
+
+.score-container__overall-score {
+ color: #FFF;
+ font-size: 92px;
+ font-weight: 100;
+ position: relative;
+ display: inline-block;
+ text-align: center;
+ min-width: 70px;
+}
+
+.score-container__overall-score::after {
+ content: 'Your score';
+ position: absolute;
+ bottom: -4px;
+ font-size: 14px;
+ font-weight: 500;
+ text-align: center;
+ width: 100%;
+ left: 0;
+ opacity: 0.5;
+}
+
+.score-container__max-score {
+ color: #57A0A8;
+ font-size: 28px;
+ font-weight: 500;
+}
+
+.report-body__content {
+ padding: 0 5%;
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+}
+
+.report-body__menu {
+ width: 22%;
+ min-width: 230px;
+ margin: -54px 3% 0 -20px;
+ background: #FFFFFF;
+ box-shadow: 0px 2px 3px 0px rgba(0,0,0,0.20);
+ border-radius: 2px;
+}
+
+.menu__header {
+ border-radius: 2px 2px 0 0;
+ background: #3F545F;
+ padding: 0 20px;
+ height: 54px;
+ line-height: 54px;
+ color: #FFF;
+ font-family: 'Roboto', Arial, sans-serif;
+ font-size: 18px;
+}
+
+.menu__nav {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.menu__nav-item {
+ height: 40px;
+ line-height: 40px;
+ border-top: 1px solid #EBEBEB;
+}
+
+.menu__link {
+ padding: 0 20px;
+ text-decoration: none;
+ color: #777;
+ display: flex;
+}
+
+.menu__link:hover {
+ background-color: #57A0A8;
+ color: #FFF;
+}
+
+.menu__link-label {
+ flex: 1;
+ color: #49525F;
+ font-weight: 500;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.menu__link-score {
+ padding-left: 20px;
+}
+
+.menu__link-score--good,
+.report-section__score--good,
+.report-section__item-value--good {
+ color: #76B530;
+}
+
+.menu__link-score--average,
+.report-section__score--average,
+.report-section__item-value--average {
+ color: #F5A623;
+}
+
+.menu__link-score--poor,
+.report-section__score--poor,
+.report-section__item-value--poor {
+ color: #D0021B;
+}
+
+.menu__link:hover .menu__link-label,
+.menu__link:hover .menu__link-score {
+ color: #FFF;
+}
+
+.report-body__breakdown {
+ flex: 1;
+ padding-top: 20px;
+}
+
+.report-body__breakdown-item {
+ padding-bottom: 40px;
+ border-bottom: 1px solid #EBEBEB;
+}
+
+.report-body__breakdown-item:last-of-type {
+ border: none;
+}
+
+.report-body__header {
+ height: 80px;
+ border-bottom: 1px solid #EBEBEB;
+}
+
+.report-section__title {
+ -webkit-font-smoothing: antialiased;
+ font-family: 'Roboto', Arial, sans-serif;
+ font-size: 28px;
+ font-weight: 400;
+ color: #49525F;
+ display: flex;
+}
+
+.report-section__subtitle {
+ -webkit-font-smoothing: antialiased;
+ font-family: 'Roboto', Arial, sans-serif;
+ font-size: 16px;
+ font-weight: normal;
+ color: #719EA8;
+}
+
+.report-section__label {
+ flex: 1;
+}
+
+.report-section__individual-results {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.report-section__item {
+ display: flex;
+ padding-left: 32px;
+ background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNXB4IiBoZWlnaHQ9IjVweCIgdmlld0JveD0iMCAwIDUgNSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxjaXJjbGUgaWQ9Ik92YWwtNzAiIHN0cm9rZT0ibm9uZSIgZmlsbD0iIzY0NjQ2NCIgZmlsbC1ydWxlPSJldmVub2RkIiBjeD0iMi41IiBjeT0iMi41IiByPSIyLjUiPjwvY2lyY2xlPgo8L3N2Zz4K') 14px center no-repeat;
+ line-height: 24px;
+}
+
+.report-section__item-raw-value {
+ color: #777;
+}
+
+.report-section__item-description {
+ flex: 1;
+}
+
+.footer {
+ margin-top: 40px;
+ height: 130px;
+ line-height: 90px;
+ text-align: center;
+ font-size: 12px;
+ border-top: 1px solid #EBEBEB;
+ color: #999;
+}
diff --git a/report/templates/report.html b/report/templates/report.html
new file mode 100644
index 000000000000..6600d411c5ac
--- /dev/null
+++ b/report/templates/report.html
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+ Lighthouse report
+
+
+
+