refactored css, including some markup changes

This commit is contained in:
Danny Coates 2018-02-13 11:32:59 -08:00
parent 3163edcbe4
commit 8d41111cd6
No known key found for this signature in database
GPG Key ID: 4C442633C62E00CB
62 changed files with 5731 additions and 4078 deletions

257
app/base.css Normal file
View File

@ -0,0 +1,257 @@
:root {
--pageBGColor: #fff;
--primaryControlBGColor: #0297f8;
--primaryControlFGColor: #fff;
--primaryControlHoverColor: #0287e8;
--inputTextColor: #737373;
--errorColor: #d70022;
--linkColor: #0094fb;
--textColor: #0c0c0d;
--lightTextColor: #737373;
--successControlBGColor: #05a700;
--successControlFGColor: #fff;
}
html {
background: url('../assets/send_bg.svg');
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
font-weight: 200;
background-size: 110%;
background-repeat: no-repeat;
background-position: center top;
height: 100%;
margin: auto;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
display: flex;
flex-direction: column;
margin: 0;
min-height: 100vh;
position: relative;
}
pre,
input,
select,
textarea,
button {
font-family: inherit;
margin: 0;
}
pre {
font-family: monospace;
font-size: 18px;
font-weight: 600;
display: inline-block;
}
a {
text-decoration: none;
}
.all {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
max-width: 650px;
margin: 0 auto;
padding: 0 20px;
box-sizing: border-box;
width: 96%;
}
.btn {
font-size: 15px;
font-weight: 500;
color: white;
cursor: pointer;
text-align: center;
background: var(--primaryControlBGColor);
border: 1px solid var(--primaryControlBGColor);
border-radius: 5px;
}
.btn--cancel {
color: var(--errorColor);
background: var(--pageBGColor);
font-size: 15px;
border: 0;
cursor: pointer;
text-decoration: underline;
}
.btn--cancel:disabled {
text-decoration: none;
cursor: auto;
}
.input {
border: 1px solid var(--primaryControlBGColor);
border-radius: 6px 0 0 6px;
font-size: 20px;
color: var(--inputTextColor);
font-family: 'SF Pro Text', sans-serif;
letter-spacing: 0;
line-height: 23px;
font-weight: 300;
height: 46px;
padding-left: 10px;
padding-right: 10px;
}
.input--noBtn {
border-radius: 6px;
}
.inputBtn {
background: var(--primaryControlBGColor);
border-radius: 0 6px 6px 0;
border: 1px solid var(--primaryControlBGColor);
color: white;
cursor: pointer;
/* Force flat button look */
appearance: none;
font-size: 15px;
padding-bottom: 3px;
padding-left: 10px;
padding-right: 10px;
white-space: nowrap;
}
.inputBtn:disabled {
cursor: auto;
}
.inputBtn:hover {
background-color: var(--primaryControlHoverColor);
}
.inputBtn--hidden {
display: none;
}
.cursor--pointer {
cursor: pointer;
}
.link {
color: var(--linkColor);
text-decoration: none;
}
.link:focus,
.link:active,
.link:hover {
color: var(--primaryControlHoverColor);
}
.link--action {
text-decoration: underline;
text-align: center;
}
.page {
margin: 0 auto 30px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
text-align: center;
}
.progressSection {
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
text-align: center;
font-size: 15px;
}
.progressSection__text {
color: rgba(0, 0, 0, 0.5);
letter-spacing: -0.4px;
margin-top: 24px;
margin-bottom: 74px;
}
.effect--fadeOut {
opacity: 0;
animation: fadeout 200ms linear;
}
@keyframes fadeout {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.effect--fadeIn {
opacity: 1;
animation: fadein 200ms linear;
}
@keyframes fadein {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.title {
font-size: 33px;
line-height: 40px;
margin: 20px auto;
text-align: center;
max-width: 520px;
font-family: 'SF Pro Text', sans-serif;
word-wrap: break-word;
}
.description {
font-size: 15px;
line-height: 23px;
max-width: 630px;
text-align: center;
margin: 0 auto 60px;
color: var(--textColor);
width: 92%;
}
@media (max-device-width: 768px), (max-width: 768px) {
.description {
margin: 0 auto 25px;
}
}
@media (max-device-width: 520px), (max-width: 520px) {
.input {
font-size: 22px;
padding: 10px 10px;
border-radius: 6px 6px 0 0;
}
.inputBtn {
border-radius: 0 0 6px 6px;
flex: 0 1 65px;
}
.input--noBtn {
border-radius: 6px;
}
}

View File

@ -108,7 +108,7 @@ export default function(state, emitter) {
document.getElementById('cancel-upload').hidden = 'hidden';
await delay(1000);
await fadeOut('upload-progress');
await fadeOut('.page');
openLinksInNewTab(links, false);
emitter.emit('pushState', `/share/${ownedFile.id}`);
} catch (err) {
@ -170,7 +170,7 @@ export default function(state, emitter) {
const time = Date.now() - start;
const speed = size / (time / 1000);
await delay(1000);
await fadeOut('download-progress');
await fadeOut('.page');
saveFile(f);
state.storage.totalDownloads += 1;
state.transfer.reset();

19
app/main.css Normal file
View File

@ -0,0 +1,19 @@
@import './base.css';
@import './templates/header/header.css';
@import './templates/downloadButton/downloadButton.css';
@import './templates/progress/progress.css';
@import './templates/passwordInput/passwordInput.css';
@import './templates/downloadPassword/downloadPassword.css';
@import './templates/setPasswordSection/setPasswordSection.css';
@import './templates/changePasswordSection/changePasswordSection.css';
@import './templates/footer/footer.css';
@import './templates/fxPromo/fxPromo.css';
@import './templates/selectbox/selectbox.css';
@import './templates/fileList/fileList.css';
@import './templates/file/file.css';
@import './templates/popup/popup.css';
@import './pages/welcome/welcome.css';
@import './pages/preview/preview.css';
@import './pages/share/share.css';
@import './pages/notFound/notFound.css';
@import './pages/unsupported/unsupported.css';

View File

@ -1,34 +0,0 @@
const html = require('choo/html');
const progress = require('../templates/progress');
const { fadeOut } = require('../utils');
module.exports = function(state, emit) {
const div = html`
<div id="page-one">
<div id="download" class="fadeIn">
<div id="download-progress">
<div id="dl-title" class="title">
${state.translate('downloadFinish')}
</div>
<div class="description"></div>
${progress(1)}
<div class="upload">
<div class="progress-text"></div>
</div>
</div>
<a class="send-new"
data-state="completed"
href="/"
onclick=${sendNew}>${state.translate('sendYourFilesLink')}</a>
</div>
</div>
`;
async function sendNew(e) {
e.preventDefault();
await fadeOut('download');
emit('pushState', '/');
}
return div;
};

View File

@ -0,0 +1,28 @@
const html = require('choo/html');
const progress = require('../../templates/progress');
const { fadeOut } = require('../../utils');
module.exports = function(state, emit) {
const div = html`
<div class="page effect--fadeIn">
<div class="title">
${state.translate('downloadFinish')}
</div>
<div class="description"></div>
${progress(1)}
<div class="progressSection">
<div class="progressSection__text"></div>
</div>
<a class="link link--action"
href="/"
onclick=${sendNew}>${state.translate('sendYourFilesLink')}</a>
</div>`;
async function sendNew(e) {
e.preventDefault();
await fadeOut('.page');
emit('pushState', '/');
}
return div;
};

View File

@ -1,46 +0,0 @@
const html = require('choo/html');
const progress = require('../templates/progress');
const { bytes } = require('../utils');
module.exports = function(state, emit) {
const transfer = state.transfer;
const cancelBtn = html`
<button
id="cancel-upload"
title="${state.translate('deletePopupCancel')}"
onclick=${cancel}>
${state.translate('deletePopupCancel')}
</button>`;
const div = html`
<div id="page-one">
<div id="download">
<div id="download-progress" class="fadeIn">
<div id="dl-title" class="title">
${state.translate('downloadingPageProgress', {
filename: state.fileInfo.name,
size: bytes(state.fileInfo.size)
})}
</div>
<div class="description">
${state.translate('downloadingPageMessage')}
</div>
${progress(transfer.progressRatio)}
<div class="upload">
<div class="progress-text">
${state.translate(transfer.msg, transfer.sizes)}
</div>
${transfer.state === 'downloading' ? cancelBtn : null}
</div>
</div>
</div>
</div>
`;
function cancel() {
const btn = document.getElementById('cancel-upload');
btn.remove();
emit('cancel');
}
return div;
};

View File

@ -0,0 +1,43 @@
const html = require('choo/html');
const progress = require('../../templates/progress');
const { bytes } = require('../../utils');
module.exports = function(state, emit) {
const transfer = state.transfer;
const cancelBtn = html`
<button
id="cancel"
class="btn btn--cancel"
title="${state.translate('deletePopupCancel')}"
onclick=${cancel}>
${state.translate('deletePopupCancel')}
</button>`;
const div = html`
<div class="page effect--fadeIn">
<div class="title">
${state.translate('downloadingPageProgress', {
filename: state.fileInfo.name,
size: bytes(state.fileInfo.size)
})}
</div>
<div class="description">
${state.translate('downloadingPageMessage')}
</div>
${progress(transfer.progressRatio)}
<div class="progressSection">
<div class="progressSection__text">
${state.translate(transfer.msg, transfer.sizes)}
</div>
${transfer.state === 'downloading' ? cancelBtn : null}
</div>
</div>
`;
function cancel() {
const btn = document.getElementById('cancel');
btn.remove();
emit('cancel');
}
return div;
};

11
app/pages/error/error.css Normal file
View File

@ -0,0 +1,11 @@
.errorPage {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
text-align: center;
}
.errorPage__img {
margin: 51px 0 71px;
}

View File

@ -1,10 +1,10 @@
const html = require('choo/html');
const assets = require('../../common/assets');
const assets = require('../../../common/assets');
module.exports = function(state) {
return html`
<div id="upload-error">
<div class="errorPage">
<div class="title">${state.translate('errorPageHeader')}</div>
<img id="upload-error-img" src="${assets.get('illustration_error.svg')}"/>
<img class="errorPage__img" src="${assets.get('illustration_error.svg')}"/>
</div>`;
};

View File

@ -12,23 +12,21 @@ function replaceLinks(str, urls) {
module.exports = function(state) {
const div = html`
<div id="page-one">
<div id="legal">
<div class="title">${state.translate('legalHeader')}</div>
${raw(
replaceLinks(state.translate('legalNoticeTestPilot'), [
'https://testpilot.firefox.com/terms',
'https://testpilot.firefox.com/privacy',
'https://testpilot.firefox.com/experiments/send'
])
)}
${raw(
replaceLinks(state.translate('legalNoticeMozilla'), [
'https://www.mozilla.org/privacy/websites/',
'https://www.mozilla.org/about/legal/terms/mozilla/'
])
)}
</div>
<div id="legal">
<div class="title">${state.translate('legalHeader')}</div>
${raw(
replaceLinks(state.translate('legalNoticeTestPilot'), [
'https://testpilot.firefox.com/terms',
'https://testpilot.firefox.com/privacy',
'https://testpilot.firefox.com/experiments/send'
])
)}
${raw(
replaceLinks(state.translate('legalNoticeMozilla'), [
'https://www.mozilla.org/privacy/websites/',
'https://www.mozilla.org/about/legal/terms/mozilla/'
])
)}
</div>
`;
return div;

View File

@ -1,21 +1,19 @@
const html = require('choo/html');
const assets = require('../../common/assets');
const assets = require('../../../common/assets');
module.exports = function(state) {
const div = html`
<div id="page-one">
<div id="download">
<div class="notFoundPage">
<div class="title">${state.translate('expiredPageHeader')}</div>
<div class="share-window">
<div class="notFoundPage__img">
<img src="${assets.get('illustration_expired.svg')}" id="expired-img">
</div>
<div class="expired-description">
<div class="description">
${state.translate('uploadPageExplainer')}
</div>
<a class="send-new" href="/" data-state="notfound">
<a class="link link--action" href="/">
${state.translate('sendYourFilesLink')}
</a>
</div>
</div>`;
</div>`;
return div;
};

View File

@ -0,0 +1,17 @@
.notFoundPage {
margin: 0 auto 30px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
text-align: center;
}
.notFoundPage__img {
margin: 0 auto;
display: flex;
justify-content: center;
flex-direction: column;
width: 100%;
max-width: 640px;
}

View File

@ -1,44 +0,0 @@
const html = require('choo/html');
const assets = require('../../common/assets');
const { bytes } = require('../utils');
module.exports = function(state, pageAction) {
const fileInfo = state.fileInfo;
const size = fileInfo.size
? state.translate('downloadFileSize', { size: bytes(fileInfo.size) })
: '';
const title = fileInfo.name
? state.translate('downloadFileName', { filename: fileInfo.name })
: state.translate('downloadFileTitle');
const info = html`
<div id="dl-file"
data-nonce="${fileInfo.nonce}"
data-requires-password="${fileInfo.requiresPassword}"></div>`;
if (!pageAction) {
return info;
}
const div = html`
<div id="page-one">
<div id="download">
<div id="download-page-one">
<div class="title">
<span>${title}</span>
<span id="dl-filesize">${' ' + size}</span>
</div>
<div class="description">${state.translate('downloadMessage')}</div>
<img
src="${assets.get('illustration_download.svg')}"
id="download-img"
title="${state.translate('downloadAltText')}"/>
${pageAction}
</div>
<a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a>
</div>
${info}
</div>
`;
return div;
};

View File

@ -0,0 +1,42 @@
const html = require('choo/html');
const assets = require('../../../common/assets');
const { bytes } = require('../../utils');
module.exports = function(state, pageAction) {
const fileInfo = state.fileInfo;
const size = fileInfo.size
? state.translate('downloadFileSize', { size: bytes(fileInfo.size) })
: '';
const title = fileInfo.name
? state.translate('downloadFileName', { filename: fileInfo.name })
: state.translate('downloadFileTitle');
const info = html`
<div id="dl-file"
data-nonce="${fileInfo.nonce}"
data-requires-password="${fileInfo.requiresPassword}"></div>`;
if (!pageAction) {
return info;
}
const div = html`
<div class="page">
<div class="title">
<span>${title}</span>
<span>${' ' + size}</span>
</div>
<div class="description">${state.translate('downloadMessage')}</div>
<img
src="${assets.get('illustration_download.svg')}"
class="preview__img"
title="${state.translate('downloadAltText')}"/>
${pageAction}
<a class="link link--action" href="/">
${state.translate('sendYourFilesLink')}
</a>
${info}
</div>
`;
return div;
};

View File

@ -0,0 +1,4 @@
.preview__img {
width: 283px;
height: 196px;
}

View File

@ -1,12 +1,13 @@
/* global EXPIRE_SECONDS */
const html = require('choo/html');
const raw = require('choo/html/raw');
const assets = require('../../common/assets');
const notFound = require('./notFound');
const uploadPasswordSet = require('../templates/uploadPasswordSet');
const uploadPasswordUnset = require('../templates/uploadPasswordUnset');
const selectbox = require('../templates/selectbox');
const { allowedCopy, delay, fadeOut } = require('../utils');
const assets = require('../../../common/assets');
const notFound = require('../notFound');
const changePasswordSection = require('../../templates/changePasswordSection');
const setPasswordSection = require('../../templates/setPasswordSection');
const selectbox = require('../../templates/selectbox');
const deletePopup = require('../../templates/popup');
const { allowedCopy, delay, fadeOut } = require('../../utils');
function expireInfo(file, translate, emit) {
const hours = Math.floor(EXPIRE_SECONDS / 60 / 60);
@ -34,44 +35,42 @@ module.exports = function(state, emit) {
}
const passwordSection = file.hasPassword
? uploadPasswordSet(state, emit)
: uploadPasswordUnset(state, emit);
? changePasswordSection(state, emit)
: setPasswordSection(state, emit);
const div = html`
<div id="share-link" class="fadeIn">
<div id="shareWrapper" class="effect--fadeIn">
<div class="title">${expireInfo(file, state.translate, emit)}</div>
<div id="share-window">
<div id="copy-text">
<div class="sharePage">
<div class="sharePage__copyText">
${state.translate('copyUrlFormLabelWithName', { filename: file.name })}
</div>
<div id="copy">
<input id="link" type="url" value="${file.url}" readonly="true"/>
<button id="copy-btn"
class="btn"
<div class="copySection">
<input
id="fileUrl"
class="copySection__url"
type="url"
value="${file.url}"
readonly="true"/>
<button id="copyBtn"
class="inputBtn inputBtn--copy"
title="${state.translate('copyUrlFormButton')}"
onclick=${copyLink}>${state.translate('copyUrlFormButton')}</button>
</div>
${passwordSection}
<button id="delete-file"
class="btn"
<button
class="btn btn--delete"
title="${state.translate('deleteFileButton')}"
onclick=${showPopup}>${state.translate('deleteFileButton')}
</button>
<div id="deletePopup" class="popup">
<div class="popuptext" onblur=${cancel} tabindex="-1">
<div class="popup-message">${state.translate('deletePopupText')}
</div>
<div class="popup-action">
<span class="popup-no" onclick=${cancel}>
${state.translate('deletePopupCancel')}
</span>
<span class="popup-yes" onclick=${deleteFile}>
${state.translate('deletePopupYes')}
</span>
</div>
</div>
<div class="sharePage__deletePopup">
${deletePopup(
state.translate('deletePopupText'),
state.translate('deletePopupYes'),
state.translate('deletePopupCancel'),
deleteFile
)}
</div>
<a class="send-new"
data-state="completed"
<a class="link link--action"
href="/"
onclick=${sendNew}>${state.translate('sendAnotherFileLink')}</a>
</div>
@ -79,48 +78,40 @@ module.exports = function(state, emit) {
`;
function showPopup() {
const popupText = document.querySelector('.popuptext');
popupText.classList.add('show');
popupText.focus();
}
function cancel(e) {
e.stopPropagation();
const popupText = document.querySelector('.popuptext');
popupText.classList.remove('show');
const popup = document.querySelector('.popup');
popup.classList.add('popup--show');
popup.focus();
}
async function sendNew(e) {
e.preventDefault();
await fadeOut('share-link');
await fadeOut('#shareWrapper');
emit('pushState', '/');
}
async function copyLink() {
if (allowedCopy()) {
emit('copy', { url: file.url, location: 'success-screen' });
const input = document.getElementById('link');
const input = document.getElementById('fileUrl');
input.disabled = true;
const copyBtn = document.getElementById('copy-btn');
const copyBtn = document.getElementById('copyBtn');
copyBtn.disabled = true;
copyBtn.classList.add('success');
copyBtn.classList.add('inputBtn--copied');
copyBtn.replaceChild(
html`<img src="${assets.get('check-16.svg')}" class="icon-check">`,
html`<img src="${assets.get('check-16.svg')}" class="cursor--pointer">`,
copyBtn.firstChild
);
await delay(2000);
input.disabled = false;
if (!copyBtn.parentNode.classList.contains('wait-password')) {
copyBtn.disabled = false;
}
copyBtn.classList.remove('success');
copyBtn.disabled = false;
copyBtn.classList.remove('inputBtn--copied');
copyBtn.textContent = state.translate('copyUrlFormButton');
}
}
async function deleteFile() {
emit('delete', { file, location: 'success-screen' });
await fadeOut('share-link');
await fadeOut('#shareWrapper');
emit('pushState', '/');
}
return div;

109
app/pages/share/share.css Normal file
View File

@ -0,0 +1,109 @@
.sharePage {
margin: 0 auto;
display: flex;
justify-content: center;
flex-direction: column;
width: 100%;
max-width: 640px;
}
.sharePage__copyText {
align-self: flex-start;
margin-top: 60px;
margin-bottom: 10px;
color: var(--textColor);
max-width: 614px;
word-wrap: break-word;
}
.sharePage__deletePopup {
position: relative;
align-self: center;
bottom: 50px;
}
.copySection {
display: flex;
flex-wrap: nowrap;
width: 100%;
}
.copySection__url {
flex: 1;
height: 56px;
border: 1px solid var(--primaryControlBGColor);
border-radius: 6px 0 0 6px;
font-size: 20px;
color: var(--inputTextColor);
font-family: 'SF Pro Text', sans-serif;
letter-spacing: 0;
line-height: 23px;
font-weight: 300;
padding-left: 10px;
padding-right: 10px;
}
.copySection__url:disabled {
border: 1px solid var(--successControlBGColor);
background: var(--successControlFGColor);
}
.inputBtn--copy {
flex: 0 1 165px;
padding-bottom: 4px;
}
.inputBtn--copied,
.inputBtn--copied:hover {
background: var(--successControlBGColor);
border: 1px solid var(--successControlBGColor);
color: var(--successControlFGColor);
}
.btn--delete {
align-self: center;
width: 176px;
height: 44px;
background: #fff;
border-color: rgba(12, 12, 13, 0.3);
margin-top: 50px;
margin-bottom: 12px;
color: #313131;
}
.btn--delete:hover {
background: #efeff1;
}
@media (max-device-width: 768px), (max-width: 768px) {
.copySection {
width: 100%;
}
.copySection__url {
font-size: 18px;
}
}
@media (max-device-width: 520px), (max-width: 520px) {
.copySection {
width: 100%;
flex-direction: column;
padding-left: 0;
}
.copySection__url {
font-size: 22px;
padding: 15px 10px;
border-radius: 6px 6px 0 0;
}
.sharePage__copyText {
text-align: center;
}
.inputBtn--copy {
border-radius: 0 0 6px 6px;
flex: 0 1 65px;
}
}

View File

@ -1,52 +0,0 @@
const html = require('choo/html');
const assets = require('../../common/assets');
module.exports = function(state) {
const msg =
state.params.reason === 'outdated'
? html`
<div id="unsupported-browser">
<div class="title">${state.translate('notSupportedHeader')}</div>
<div class="description">
${state.translate('notSupportedOutdatedDetail')}
</div>
<a
id="update-firefox"
href="https://support.mozilla.org/kb/update-firefox-latest-version">
<img
src="${assets.get('firefox_logo-only.svg')}"
class="firefox-logo"
alt="Firefox"/>
<div class="unsupported-button-text">
${state.translate('updateFirefox')}
</div>
</a>
<div class="unsupported-description">
${state.translate('uploadPageExplainer')}
</div>
</div>`
: html`
<div id="unsupported-browser">
<div class="title">${state.translate('notSupportedHeader')}</div>
<div class="description">${state.translate('notSupportedDetail')}</div>
<div class="description">
<a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported">
${state.translate('notSupportedLink')}
</a>
</div>
<a id="dl-firefox" href="https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com">
<img
src="${assets.get('firefox_logo-only.svg')}"
class="firefox-logo"
alt="Firefox"/>
<div class="unsupported-button-text">Firefox<br>
<span>${state.translate('downloadFirefoxButtonSub')}</span>
</div>
</a>
<div class="unsupported-description">
${state.translate('uploadPageExplainer')}
</div>
</div>`;
const div = html`<div id="page-one">${msg}</div>`;
return div;
};

View File

@ -0,0 +1,68 @@
const html = require('choo/html');
const assets = require('../../../common/assets');
function outdatedStrings(state) {
return {
title: state.translate('notSupportedHeader'),
description: state.translate('notSupportedOutdatedDetail'),
button: state.translate('updateFirefox'),
explainer: state.translate('uploadPageExplainer')
};
}
function unsupportedStrings(state) {
return {
title: state.translate('notSupportedHeader'),
description: state.translate('notSupportedDetail'),
button: state.translate('downloadFirefoxButtonSub'),
explainer: state.translate('uploadPageExplainer')
};
}
module.exports = function(state) {
let strings = {};
let why = '';
let url = '';
let buttonAction = '';
if (state.params.reason !== 'outdated') {
strings = unsupportedStrings(state);
why = html`
<div class="description">
<a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported">
${state.translate('notSupportedLink')}
</a>
</div>`;
url =
'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com';
buttonAction = html`
<div class="firefoxDownload__action">
Firefox<br><span class="firefoxDownload__text">${strings.button}</span>
</div>`;
} else {
strings = outdatedStrings(state);
url = 'https://support.mozilla.org/kb/update-firefox-latest-version';
buttonAction = html`
<div class="firefoxDownload__action">
${strings.button}
</div>`;
}
const div = html`
<div class="unsupportedPage">
<div class="title">${strings.title}</div>
<div class="description">
${strings.description}
</div>
${why}
<a href="${url}" class="firefoxDownload">
<img
src="${assets.get('firefox_logo-only.svg')}"
class="firefoxDownload__logo"
alt="Firefox"/>
${buttonAction}
</a>
<div class="unsupportedPage__info">
${strings.explainer}
</div>
</div>`;
return div;
};

View File

@ -0,0 +1,49 @@
.unsupportedPage {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.unsupportedPage__info {
font-size: 13px;
line-height: 23px;
text-align: center;
color: #7d7d7d;
margin: 0 auto 23px;
}
.firefoxDownload {
margin-bottom: 181px;
height: 80px;
background: #98e02b;
border-radius: 3px;
cursor: pointer;
border: 0;
box-shadow: 0 5px 3px rgb(234, 234, 234);
font-family: 'Fira Sans', 'segoe ui', sans-serif;
font-weight: 500;
color: #fff;
font-size: 26px;
display: flex;
justify-content: center;
align-items: center;
line-height: 1;
padding: 0 25px;
}
.firefoxDownload__logo {
width: 70px;
}
.firefoxDownload__action {
text-align: left;
margin-left: 20.4px;
}
.firefoxDownload__text {
font-family: 'Fira Sans', 'segoe ui', sans-serif;
font-weight: 300;
font-size: 18px;
letter-spacing: -0.69px;
}

View File

@ -1,41 +0,0 @@
const html = require('choo/html');
const progress = require('../templates/progress');
const { bytes } = require('../utils');
module.exports = function(state, emit) {
const transfer = state.transfer;
const div = html`
<div id="download">
<div id="upload-progress" class="fadeIn">
<div class="title" id="upload-filename">
${state.translate('uploadingPageProgress', {
filename: transfer.file.name,
size: bytes(transfer.file.size)
})}
</div>
<div class="description"></div>
${progress(transfer.progressRatio)}
<div class="upload">
<div class="progress-text">
${state.translate(transfer.msg, transfer.sizes)}
</div>
<button
id="cancel-upload"
title="${state.translate('uploadingPageCancel')}"
onclick=${cancel}>
${state.translate('uploadingPageCancel')}
</button>
</div>
</div>
</div>
`;
function cancel() {
const btn = document.getElementById('cancel-upload');
btn.disabled = true;
btn.textContent = state.translate('uploadCancelNotification');
emit('cancel');
}
return div;
};

40
app/pages/upload/index.js Normal file
View File

@ -0,0 +1,40 @@
const html = require('choo/html');
const progress = require('../../templates/progress');
const { bytes } = require('../../utils');
module.exports = function(state, emit) {
const transfer = state.transfer;
const div = html`
<div class="page effect--fadeIn">
<div class="title">
${state.translate('uploadingPageProgress', {
filename: transfer.file.name,
size: bytes(transfer.file.size)
})}
</div>
<div class="description"></div>
${progress(transfer.progressRatio)}
<div class="progressSection">
<div class="progressSection__text">
${state.translate(transfer.msg, transfer.sizes)}
</div>
<button
id="cancel-upload"
class="btn btn--cancel"
title="${state.translate('uploadingPageCancel')}"
onclick=${cancel}>
${state.translate('uploadingPageCancel')}
</button>
</div>
</div>
`;
function cancel() {
const btn = document.getElementById('cancel-upload');
btn.disabled = true;
btn.textContent = state.translate('uploadCancelNotification');
emit('cancel');
}
return div;
};

View File

@ -1,12 +1,12 @@
/* global MAXFILESIZE */
const html = require('choo/html');
const assets = require('../../common/assets');
const fileList = require('../templates/fileList');
const { bytes, fadeOut } = require('../utils');
const assets = require('../../../common/assets');
const fileList = require('../../templates/fileList');
const { bytes, fadeOut } = require('../../utils');
module.exports = function(state, emit) {
// the page flickers if both the server and browser set 'fadeIn'
const fade = state.layout ? '' : 'fadeIn';
// the page flickers if both the server and browser set 'effect--fadeIn'
const fade = state.layout ? '' : 'effect--fadeIn';
const div = html`
<div id="page-one" class="${fade}">
<div class="title">${state.translate('uploadPageHeader')}</div>
@ -18,7 +18,7 @@ module.exports = function(state, emit) {
${state.translate('uploadPageLearnMore')}
</a>
</div>
<div class="upload-window"
<div class="uploadArea"
ondragover=${dragover}
ondragleave=${dragleave}>
<div id="upload-img">
@ -26,19 +26,21 @@ module.exports = function(state, emit) {
src="${assets.get('upload.svg')}"
title="${state.translate('uploadSvgAlt')}"/>
</div>
<div id="upload-text">${state.translate('uploadPageDropMessage')}</div>
<span id="file-size-msg">
<em>${state.translate('uploadPageSizeMessage')}</em>
<div class="uploadArea__msg">
${state.translate('uploadPageDropMessage')}
</div>
<span class="uploadArea__sizeMsg">
${state.translate('uploadPageSizeMessage')}
</span>
<input id="file-upload"
class="inputFile"
type="file"
name="fileUploaded"
onfocus=${onfocus}
onblur=${onblur}
onchange=${upload} />
<label for="file-upload"
id="browse"
class="btn browse"
class="btn btn--file"
title="${state.translate('uploadPageBrowseButton1')}">
${state.translate('uploadPageBrowseButton1')}
</label>
@ -48,21 +50,21 @@ module.exports = function(state, emit) {
`;
function dragover(event) {
const div = document.querySelector('.upload-window');
div.classList.add('ondrag');
const div = document.querySelector('.uploadArea');
div.classList.add('uploadArea--dragging');
}
function dragleave(event) {
const div = document.querySelector('.upload-window');
div.classList.remove('ondrag');
const div = document.querySelector('.uploadArea');
div.classList.remove('uploadArea--dragging');
}
function onfocus(event) {
event.target.classList.add('has-focus');
event.target.classList.add('inputFile--focused');
}
function onblur(event) {
event.target.classList.remove('has-focus');
event.target.classList.remove('inputFile--focused');
}
async function upload(event) {
@ -77,7 +79,7 @@ module.exports = function(state, emit) {
return;
}
await fadeOut('page-one');
await fadeOut('#page-one');
emit('upload', { file, type: 'click' });
}
return div;

View File

@ -0,0 +1,71 @@
.uploadArea {
border: 3px dashed rgba(0, 148, 251, 0.5);
margin: 0 auto 10px;
height: 255px;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
text-align: center;
transition: transform 150ms;
padding: 15px;
}
.uploadArea__msg {
font-size: 22px;
color: var(--lightTextColor);
margin: 20px 0 10px;
font-family: 'SF Pro Text', sans-serif;
}
.uploadArea__sizeMsg {
font-style: italic;
font-size: 12px;
line-height: 16px;
color: var(--lightTextColor);
margin-bottom: 22px;
}
.uploadArea--dragging {
border: 5px dashed rgba(0, 148, 251, 0.5);
height: 251px;
transform: scale(1.04);
border-radius: 4.2px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
text-align: center;
}
.uploadArea--dragging * {
pointer-events: none;
}
.btn--file {
font-size: 20px;
min-width: 240px;
height: 60px;
display: flex;
justify-content: center;
align-items: center;
padding: 0 10px;
}
.btn--file:hover {
background-color: var(--primaryControlHoverColor);
}
.inputFile {
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.btn--file .inputFile--focused {
background-color: var(--primaryControlHoverColor);
outline: 1px dotted #000;
outline: -webkit-focus-ring-color auto 5px;
}

View File

@ -0,0 +1,29 @@
.changePasswordSection {
padding: 10px 0;
align-self: left;
max-width: 100%;
overflow-wrap: break-word;
}
.btn--reset {
width: 80px;
height: 30px;
background: #fff;
border-color: rgba(12, 12, 13, 0.3);
margin-top: 5px;
margin-left: 15px;
margin-bottom: 12px;
line-height: 24px;
color: #313131;
}
.btn--reset:hover {
background: #efeff1;
}
@media (max-device-width: 520px), (max-width: 520px) {
.changePasswordSection {
align-self: center;
min-width: 95%;
}
}

View File

@ -0,0 +1,52 @@
const html = require('choo/html');
const raw = require('choo/html/raw');
const passwordInput = require('../passwordInput');
module.exports = function(state, emit) {
const file = state.storage.getFileById(state.params.id);
return html`<div class="changePasswordSection">
${passwordSpan(file.password)}
<button
class="btn btn--reset"
onclick=${toggleResetInput}
>${state.translate('changePasswordButton')}</button>
${passwordInput(
state.translate('unlockInputPlaceholder'),
state.translate('changePasswordButton'),
changePassword
)}
</div>`;
function passwordSpan(password) {
password = password || '●●●●●';
const span = html`<span>${raw(
state.translate('passwordResult', {
password: '<pre class="passwordMask"></pre>'
})
)}</span>`;
const masked = span.querySelector('.passwordMask');
masked.textContent = password.replace(/./g, '●');
return span;
}
function changePassword(event) {
event.preventDefault();
const password = document.getElementById('password-input').value;
if (password.length > 0) {
emit('password', { password, file });
}
return false;
}
function toggleResetInput(event) {
const form = event.target.parentElement.querySelector('form.passwordInput');
const input = document.getElementById('password-input');
if (form.style.visibility === 'hidden' || form.style.visibility === '') {
form.style.visibility = 'visible';
input.focus();
} else {
form.style.visibility = 'hidden';
}
}
};

View File

@ -0,0 +1,10 @@
.btn--download {
width: 180px;
height: 44px;
margin-top: 20px;
margin-bottom: 30px;
}
.btn--download:hover {
background-color: var(--primaryControlHoverColor);
}

View File

@ -8,8 +8,7 @@ module.exports = function(state, emit) {
return html`
<div>
<button id="download-btn"
class="btn"
<button class="btn btn--download"
onclick=${download}>${state.translate('downloadButtonLabel')}
</button>
</div>`;

View File

@ -0,0 +1,30 @@
.passwordSection {
text-align: left;
padding: 40px;
}
.passwordForm {
display: flex;
flex-wrap: nowrap;
width: 100%;
padding: 10px 0;
}
.input--password {
flex: 1;
}
.inputBtn--password {
flex: 0 1 165px;
}
.red {
color: red;
}
@media (max-device-width: 520px), (max-width: 520px) {
.passwordForm {
flex-direction: column;
padding-left: 0;
}
}

View File

@ -5,52 +5,52 @@ module.exports = function(state, emit) {
const label =
fileInfo.password === null
? html`
<label class="red" for="unlock-input">
<label class="red" for="password-input">
${state.translate('passwordTryAgain')}
</label>`
: html`
<label for="unlock-input">
<label for="password-input">
${state.translate('unlockInputLabel')}
</label>`;
const div = html`
<div class="enterPassword">
<div class="passwordSection">
${label}
<form id="unlock" onsubmit=${checkPassword} data-no-csrf>
<input id="unlock-input"
class="unlock-input input-no-btn"
<form class="passwordForm" onsubmit=${checkPassword} data-no-csrf>
<input id="password-input"
class="input input--password input--noBtn"
maxlength="64"
autocomplete="off"
placeholder="${state.translate('unlockInputPlaceholder')}"
oninput=${inputChanged}
type="password" />
<input type="submit"
id="unlock-btn"
class="btn btn-hidden"
id="password-btn"
class="inputBtn inputBtn--password inputBtn--hidden"
value="${state.translate('unlockButtonLabel')}"/>
</form>
</div>`;
if (!(div instanceof String)) {
setTimeout(() => document.querySelector('#unlock-input').focus());
setTimeout(() => document.getElementById('password-input').focus());
}
function inputChanged() {
const input = document.getElementById('unlock-input');
const btn = document.getElementById('unlock-btn');
const input = document.getElementById('password-input');
const btn = document.getElementById('password-btn');
if (input.value.length > 0) {
btn.classList.remove('btn-hidden');
input.classList.remove('input-no-btn');
btn.classList.remove('inputBtn--hidden');
input.classList.remove('input--noBtn');
} else {
btn.classList.add('btn-hidden');
input.classList.add('input-no-btn');
btn.classList.add('inputBtn--hidden');
input.classList.add('input--noBtn');
}
}
function checkPassword(event) {
event.preventDefault();
const password = document.getElementById('unlock-input').value;
const password = document.getElementById('password-input').value;
if (password.length > 0) {
document.getElementById('unlock-btn').disabled = true;
document.getElementById('password-btn').disabled = true;
state.fileInfo.url = window.location.href;
state.fileInfo.password = password;
emit('getMetadata');

View File

@ -0,0 +1,26 @@
.fileData {
font-size: 15px;
vertical-align: top;
color: #4a4a4a;
padding: 17px 19px 0;
line-height: 23px;
position: relative;
}
.fileData--overflow {
text-overflow: ellipsis;
max-width: 0;
overflow: hidden;
white-space: nowrap;
}
.fileData--center {
text-align: center;
}
@media (max-device-width: 520px), (max-width: 520px) {
.fileData {
font-size: 13px;
padding: 17px 5px 0;
}
}

View File

@ -1,6 +1,7 @@
const html = require('choo/html');
const assets = require('../../common/assets');
const number = require('../utils').number;
const assets = require('../../../common/assets');
const number = require('../../utils').number;
const deletePopup = require('../popup');
function timeLeft(milliseconds, state) {
const minutes = Math.floor(milliseconds / 1000 / 60);
@ -27,42 +28,35 @@ module.exports = function(file, state, emit) {
const totalDownloads = file.dtotal || 0;
const row = html`
<tr id="${file.id}">
<td class="overflow-col" title="${file.name}">
<td class="fileData fileData--overflow" title="${file.name}">
<a class="link" href="/share/${file.id}">${file.name}</a>
</td>
<td class="center-col">
<td class="fileData fileData--center">
<img
onclick=${copyClick}
src="${assets.get('copy-16.svg')}"
class="icon-copy"
class="cursor--pointer"
title="${state.translate('copyUrlHover')}">
<span class="text-copied" hidden="true">
<span hidden="true">
${state.translate('copiedUrl')}
</span>
</td>
<td class="overflow-col">${remainingTime}</td>
<td class="center-col">${number(totalDownloads)} / ${number(
<td class="fileData fileData--overflow">${remainingTime}</td>
<td class="fileData fileData--center">${number(totalDownloads)} / ${number(
downloadLimit
)}</td>
<td class="center-col">
<td class="fileData fileData--center">
<img
onclick=${showPopup}
src="${assets.get('close-16.svg')}"
class="icon-delete"
class="cursor--pointer"
title="${state.translate('deleteButtonHover')}">
<div class="popup">
<div class="popuptext" onblur=${cancel} tabindex="-1">
<div class="popup-message">${state.translate('deletePopupText')}</div>
<div class="popup-action">
<span class="popup-no" onclick=${cancel}>
${state.translate('deletePopupCancel')}
</span>
<span class="popup-yes" onclick=${deleteFile}>
${state.translate('deletePopupYes')}
</span>
</div>
</div>
</div>
${deletePopup(
state.translate('deletePopupText'),
state.translate('deletePopupYes'),
state.translate('deletePopupCancel'),
deleteFile
)}
</td>
</tr>
`;
@ -81,18 +75,11 @@ module.exports = function(file, state, emit) {
function showPopup() {
const tr = document.getElementById(file.id);
const popup = tr.querySelector('.popuptext');
popup.classList.add('show');
const popup = tr.querySelector('.popup');
popup.classList.add('popup--show');
popup.focus();
}
function cancel(e) {
e.stopPropagation();
const tr = document.getElementById(file.id);
const popup = tr.querySelector('.popuptext');
popup.classList.remove('show');
}
function deleteFile() {
emit('delete', { file, location: 'upload-list' });
emit('render');

View File

@ -0,0 +1,52 @@
.fileList {
margin: 45.3px auto;
table-layout: fixed;
border-collapse: collapse;
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
}
.fileList__header {
font-size: 16px;
color: #858585;
font-weight: lighter;
text-align: left;
background: rgba(0, 148, 251, 0.05);
height: 40px;
border-top: 1px solid rgba(0, 148, 251, 0.1);
padding: 0 19px;
white-space: nowrap;
}
.fileList__body {
word-wrap: break-word;
word-break: break-all;
}
.fileList__nameCol {
width: 35%;
}
.fileList__copyCol {
text-align: center;
width: 25%;
}
.fileList__expireCol {
width: 25%;
}
.fileList__dlCol {
width: 8%;
}
.fileList__delCol {
text-align: center;
width: 7%;
}
@media (max-device-width: 520px), (max-width: 520px) {
.fileList__header {
font-size: 14px;
padding: 0 5px;
}
}

View File

@ -1,37 +1,35 @@
const html = require('choo/html');
const file = require('./file');
const file = require('../file');
module.exports = function(state, emit) {
let table = '';
if (state.storage.files.length) {
table = html`
<table id="uploaded-files">
<table class="fileList">
<thead>
<tr>
<th id="uploaded-file">${state.translate('uploadedFile')}</th>
<th id="copy-file-list" class="center-col">
<th class="fileList__header fileList__nameCol">
${state.translate('uploadedFile')}
</th>
<th class="fileList__header fileList__copyCol">
${state.translate('copyFileList')}
</th>
<th id="expiry-time-file-list" >
<th class="fileList__header fileList__expireCol" >
${state.translate('timeFileList')}
</th>
<th id="expiry-downloads-file-list" >
<th class="fileList__header fileList__dlCol" >
${state.translate('downloadsFileList')}
</th>
<th id="delete-file-list" class="center-col">
<th class="fileList__header fileList__delCol">
${state.translate('deleteFileList')}
</th>
</tr>
</thead>
<tbody>
<tbody class="fileList__body">
${state.storage.files.map(f => file(f, state, emit))}
</tbody>
</table>
`;
}
return html`
<div id="file-list">
${table}
</div>
`;
return table;
};

View File

@ -1,43 +0,0 @@
const html = require('choo/html');
const assets = require('../../common/assets');
module.exports = function(state) {
return html`<div class="footer">
<div class="legal-links">
<a href="https://www.mozilla.org" role="presentation">
<img
class="mozilla-logo"
src="${assets.get('mozilla-logo.svg')}"
alt="mozilla"/>
</a>
<a href="https://www.mozilla.org/about/legal">
${state.translate('footerLinkLegal')}
</a>
<a href="https://testpilot.firefox.com/about">
${state.translate('footerLinkAbout')}
</a>
<a href="/legal">${state.translate('footerLinkPrivacy')}</a>
<a href="/legal">${state.translate('footerLinkTerms')}</a>
<a href="https://www.mozilla.org/privacy/websites/#cookies">
${state.translate('footerLinkCookies')}
</a>
<a href="https://www.mozilla.org/about/legal/report-infringement/">
${state.translate('reportIPInfringement')}
</a>
</div>
<div class="social-links">
<a href="https://github.com/mozilla/send" role="presentation">
<img
class="github"
src="${assets.get('github-icon.svg')}"
alt="github"/>
</a>
<a href="https://twitter.com/FxTestPilot" role="presentation">
<img
class="twitter"
src="${assets.get('twitter-icon.svg')}"
alt="twitter"/>
</a>
</div>
</div>`;
};

View File

@ -0,0 +1,93 @@
.footer {
right: 0;
bottom: 0;
left: 0;
font-size: 13px;
display: flex;
align-items: flex-end;
flex-direction: row;
justify-content: space-between;
padding: 50px 31px 41px;
width: 100%;
box-sizing: border-box;
}
.legalSection {
max-width: 81vw;
display: flex;
align-items: center;
flex-direction: row;
}
.legalSection__link {
color: #858585;
opacity: 0.9;
white-space: nowrap;
margin-right: 2vw;
}
.legalSection__link:hover {
opacity: 1;
}
.legalSection__link:visited {
color: #858585;
}
.legalSection__mozLogo {
width: 112px;
height: 32px;
margin-bottom: -5px;
}
.socialSection {
display: flex;
justify-content: space-between;
width: 94px;
}
.socialSection__link {
opacity: 0.9;
}
.socialSection__link:hover {
opacity: 1;
}
.socialSection__icon {
width: 32px;
height: 32px;
margin-bottom: -5px;
}
@media (max-device-width: 768px), (max-width: 768px) {
.footer {
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
max-width: 630px;
margin: auto;
}
.legalSection__mozLogo {
margin-left: -7px;
}
.legalSection {
flex-direction: column;
margin: auto;
width: 100%;
max-width: 100%;
}
.legalSection__link {
display: block;
padding: 10px 0;
align-self: flex-start;
}
.socialSection {
margin-top: 20px;
align-self: flex-start;
}
}

View File

@ -0,0 +1,64 @@
const html = require('choo/html');
const assets = require('../../../common/assets');
module.exports = function(state) {
return html`<div class="footer">
<div class="legalSection">
<a
href="https://www.mozilla.org"
class="legalSection__link"
role="presentation">
<img
class="legalSection__mozLogo"
src="${assets.get('mozilla-logo.svg')}"
alt="mozilla"/>
</a>
<a
href="https://www.mozilla.org/about/legal"
class="legalSection__link">
${state.translate('footerLinkLegal')}
</a>
<a
href="https://testpilot.firefox.com/about"
class="legalSection__link">
${state.translate('footerLinkAbout')}
</a>
<a
href="/legal"
class="legalSection__link">${state.translate('footerLinkPrivacy')}</a>
<a
href="/legal"
class="legalSection__link">${state.translate('footerLinkTerms')}</a>
<a
href="https://www.mozilla.org/privacy/websites/#cookies"
class="legalSection__link">
${state.translate('footerLinkCookies')}
</a>
<a
href="https://www.mozilla.org/about/legal/report-infringement/"
class="legalSection__link">
${state.translate('reportIPInfringement')}
</a>
</div>
<div class="socialSection">
<a
href="https://github.com/mozilla/send"
class="socialSection__link"
role="presentation">
<img
class="socialSection__icon"
src="${assets.get('github-icon.svg')}"
alt="github"/>
</a>
<a
href="https://twitter.com/FxTestPilot"
class="socialSection__link"
role="presentation">
<img
class="socialSection__icon"
src="${assets.get('twitter-icon.svg')}"
alt="twitter"/>
</a>
</div>
</div>`;
};

View File

@ -0,0 +1,56 @@
.fxPromo {
padding: 0 15px;
height: 48px;
background-color: #efeff1;
color: #4a4a4f;
font-size: 13px;
display: flex;
flex-direction: row;
align-content: center;
align-items: center;
justify-content: center;
}
.fxPromo > div {
display: flex;
align-items: center;
margin: 0 auto;
}
.fxPromo > div > span {
margin-left: 10px;
}
.fxPromo__logo {
width: 24px;
}
.fxPromo--blue {
background: linear-gradient(-180deg, #45a1ff 0%, #00feff 94%);
color: #fff;
}
.fxPromo--pink {
background: linear-gradient(-180deg, #ff9400 0%, #ff1ad9 94%);
color: #fff;
}
.fxPromo--blue a {
color: #fff;
font-weight: bold;
}
.fxPromo--pink a {
color: #fff;
font-weight: bold;
}
.fxPromo--blue a:hover {
color: #eee;
font-weight: bold;
}
.fxPromo--pink a:hover {
color: #eee;
font-weight: bold;
}

View File

@ -1,17 +1,17 @@
const html = require('choo/html');
const assets = require('../../common/assets');
const assets = require('../../../common/assets');
module.exports = function(state, emit) {
function clicked() {
emit('experiment', { cd3: 'promo' });
}
let classes = 'banner';
let classes = 'fxPromo';
switch (state.promo) {
case 'blue':
classes = 'banner banner-blue';
classes = 'fxPromo fxPromo--blue';
break;
case 'pink':
classes = 'banner banner-pink';
classes = 'fxPromo fxPromo--pink';
break;
}
@ -20,7 +20,7 @@ module.exports = function(state, emit) {
<div>
<img
src="${assets.get('firefox_logo-only.svg')}"
class="firefox-logo-small"
class="fxPromo__logo"
alt="Firefox"/>
<span>Send is brought to you by the all-new Firefox.
<a

View File

@ -0,0 +1,104 @@
.header {
align-items: flex-start;
box-sizing: border-box;
display: flex;
justify-content: space-between;
padding: 31px;
width: 100%;
}
.logo {
display: flex;
position: relative;
align-items: center;
}
.logo__link {
display: flex;
flex-direction: row;
}
.logo__title {
color: #3e3d40;
font-size: 32px;
font-weight: 500;
margin: 0;
position: relative;
top: -1px;
letter-spacing: 1px;
margin-left: 8px;
transition: color 50ms;
}
.logo__title:hover {
color: var(--primaryControlBGColor);
}
.logo__subtitle {
color: #3e3d40;
font-size: 12px;
margin: 0 8px;
}
.logo__subtitle-link {
font-weight: bold;
color: #3e3d40;
transition: color 50ms;
}
.logo__subtitle-link:hover {
color: var(--primaryControlBGColor);
}
.feedback {
background-color: var(--primaryControlBGColor);
background-image: url('../assets/feedback.svg');
background-position: 2px 4px;
background-repeat: no-repeat;
background-size: 18px;
border-radius: 3px;
border: 1px solid var(--primaryControlBGColor);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
color: #fff;
cursor: pointer;
display: block;
float: right;
font-size: 12px;
line-height: 12px;
opacity: 0.9;
padding: 5px;
overflow: hidden;
min-width: 12px;
max-width: 12px;
text-indent: 17px;
transition: all 250ms ease-in-out;
white-space: nowrap;
}
.feedback:hover,
.feedback:focus {
min-width: 30px;
max-width: 300px;
text-indent: 2px;
padding: 5px 5px 5px 20px;
background-color: var(--primaryControlHoverColor);
}
.feedback:active {
background-color: #0277d8;
}
@media (max-device-width: 520px), (max-width: 520px) {
.header {
flex-direction: column;
justify-content: flex-start;
}
.feedback {
margin-top: 10px;
min-width: 30px;
max-width: 300px;
text-indent: 2px;
padding: 5px 5px 5px 20px;
}
}

View File

@ -1,5 +1,5 @@
const html = require('choo/html');
const assets = require('../../common/assets');
const assets = require('../../../common/assets');
/*
The current weback config uses package.json to generate
version.json for /__version__ meaning `require` returns the
@ -10,7 +10,7 @@ const assets = require('../../common/assets');
has a custom loader (/build/version_loader.js) just to replace
string with the value from package.json. 🤢
*/
const version = require('../../package.json').version || 'VERSION';
const version = require('../../../package.json').version || 'VERSION';
function browserName() {
try {
@ -39,15 +39,15 @@ const browser = browserName();
module.exports = function(state) {
return html`<header class="header">
<div class="send-logo">
<a href="/">
<div class="logo">
<a class="logo__link" href="/">
<img
src="${assets.get('send_logo.svg')}"
alt="Send"/>
<h1 class="site-title">Send</h1>
<h1 class="logo__title">Send</h1>
</a>
<div class="site-subtitle">
<a href="https://testpilot.firefox.com">Firefox Test Pilot</a>
<div class="logo__subtitle">
<a class="logo__subtitle-link" href="https://testpilot.firefox.com">Firefox Test Pilot</a>
<div>${state.translate('siteSubtitle')}</div>
</div>
</div>

View File

@ -0,0 +1,34 @@
const html = require('choo/html');
module.exports = function(placeholder, action, submit) {
return html`
<form
class="passwordInput passwordInput--hidden"
onsubmit=${submit}
data-no-csrf>
<input id="password-input"
class="input input--noBtn"
maxlength="32"
autocomplete="off"
type="password"
oninput=${inputChanged}
placeholder="${placeholder}">
<input type="submit"
id="password-btn"
class="inputBtn inputBtn--hidden"
value="${action}"/>
</form>
`;
function inputChanged() {
const resetInput = document.getElementById('password-input');
const resetBtn = document.getElementById('password-btn');
if (resetInput.value.length > 0) {
resetBtn.classList.remove('inputBtn--hidden');
resetInput.classList.remove('input--noBtn');
} else {
resetBtn.classList.add('inputBtn--hidden');
resetInput.classList.add('input--noBtn');
}
}
};

View File

@ -0,0 +1,18 @@
.passwordInput {
align-self: left;
display: flex;
flex-wrap: nowrap;
width: 80%;
padding: 10px 5px;
}
.passwordInput--hidden {
visibility: hidden;
}
@media (max-device-width: 520px), (max-width: 520px) {
.passwordInput {
flex-direction: column;
width: inherit;
}
}

View File

@ -0,0 +1,26 @@
const html = require('choo/html');
module.exports = function(msg, confirmText, cancelText, confirmCallback) {
function hide(e) {
e.stopPropagation();
const popup = document.querySelector('.popup.popup--show');
if (popup) {
popup.classList.remove('popup--show');
}
}
return html`
<div class="popup__wrapper">
<div class="popup" onblur=${hide} tabindex="-1">
<div class="popup__message">${msg}</div>
<div class="popup__action">
<span class="popup__no" onclick=${hide}>
${cancelText}
</span>
<span class="popup__yes" onclick=${confirmCallback}>
${confirmText}
</span>
</div>
</div>
</div>`;
};

View File

@ -0,0 +1,122 @@
.popup {
visibility: hidden;
min-width: 204px;
min-height: 105px;
background-color: #fff;
color: #000;
border: 1px solid #d7d7db;
padding: 15px 24px;
box-sizing: content-box;
text-align: center;
border-radius: 5px;
position: absolute;
z-index: 1;
bottom: 20px;
left: -40px;
transition: opacity 0.5s;
opacity: 0;
outline: 0;
box-shadow: 3px 3px 7px rgba(136, 136, 136, 0.3);
}
.popup::after {
content: '';
position: absolute;
bottom: -11px;
left: 20px;
background-color: #fff;
display: block;
width: 20px;
height: 20px;
transform: rotate(45deg);
border-radius: 0 0 5px;
border-right: 1px solid #d7d7db;
border-bottom: 1px solid #d7d7db;
border-left: 1px solid #fff;
border-top: 1px solid #fff;
}
.popup__wrapper {
position: absolute;
display: inline-block;
}
.popup__message {
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border-bottom: 1px #ebebeb solid;
color: var(--textColor);
font-size: 15px;
font-weight: normal;
padding-bottom: 15px;
white-space: nowrap;
width: calc(100% + 48px);
margin-left: -24px;
}
.popup__action {
margin-top: 15px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.popup__no {
color: #4a4a4a;
background-color: #fbfbfb;
border: 1px #c1c1c1 solid;
border-radius: 5px;
padding: 5px 25px;
font-weight: normal;
min-width: 94px;
box-sizing: border-box;
cursor: pointer;
white-space: nowrap;
}
.popup__no:hover {
background-color: #efeff1;
}
.popup__yes {
color: #fff;
background-color: var(--primaryControlBGColor);
border-radius: 5px;
padding: 5px 25px;
font-weight: normal;
cursor: pointer;
min-width: 94px;
box-sizing: border-box;
white-space: nowrap;
margin-left: 12px;
}
.popup__yes:hover {
background-color: var(--primaryControlHoverColor);
}
.popup--show {
visibility: visible;
opacity: 1;
}
@media (max-device-width: 992px), (max-width: 992px) {
.popup {
left: auto;
right: -40px;
}
.popup::after {
left: auto;
right: 36px;
}
}
@media (max-device-width: 520px), (max-width: 520px) {
.popup::after {
left: 125px;
}
}

View File

@ -1,5 +1,5 @@
const html = require('choo/html');
const percent = require('../utils').percent;
const percent = require('../../utils').percent;
const radius = 73;
const oRadius = radius + 10;
@ -10,20 +10,20 @@ module.exports = function(progressRatio) {
const dashOffset = (1 - progressRatio) * circumference;
const percentComplete = percent(progressRatio);
const div = html`
<div class="progress-bar">
<div class="progress">
<svg
id="progress"
width="${oDiameter}"
height="${oDiameter}"
viewPort="0 0 ${oDiameter} ${oDiameter}"
version="1.1">
<circle
class="progress__bg"
r="${radius}"
cx="${oRadius}"
cy="${oRadius}"
fill="transparent"/>
<circle
id="bar"
class="progress__bar"
r="${radius}"
cx="${oRadius}"
cy="${oRadius}"
@ -31,8 +31,8 @@ module.exports = function(progressRatio) {
transform="rotate(-90 ${oRadius} ${oRadius})"
stroke-dasharray="${circumference}"
stroke-dashoffset="${dashOffset}"/>
<text class="percentage" text-anchor="middle" x="50%" y="98">
<tspan class="percent-number">${percentComplete}</tspan>
<text class="progress__percent" text-anchor="middle" x="50%" y="98">
${percentComplete}
</text>
</svg>
</div>

View File

@ -0,0 +1,27 @@
.progress {
align-items: center;
display: flex;
justify-content: center;
margin-top: 3px;
position: relative;
text-align: center;
}
.progress__bg {
stroke: #eee;
stroke-width: 0.75em;
}
.progress__bar {
stroke: #3b9dff;
stroke-width: 0.75em;
transition: stroke-dashoffset 300ms linear;
}
.progress__percent {
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
font-size: 43.2px;
letter-spacing: -0.78px;
line-height: 58px;
user-select: none;
}

View File

@ -1,5 +1,5 @@
const html = require('choo/html');
const number = require('../utils').number;
const number = require('../../utils').number;
module.exports = function(selected, options, translate, changed) {
const id = `select-${Math.random()}`;
@ -8,17 +8,17 @@ module.exports = function(selected, options, translate, changed) {
function close() {
const ul = document.getElementById(id);
const body = document.querySelector('body');
ul.classList.remove('active');
ul.classList.remove('selectbox__options--active');
body.removeEventListener('click', close);
}
function toggle(event) {
event.stopPropagation();
const ul = document.getElementById(id);
if (ul.classList.contains('active')) {
if (ul.classList.contains('selectbox__options--active')) {
close();
} else {
ul.classList.add('active');
ul.classList.add('selectbox__options--active');
const body = document.querySelector('body');
body.addEventListener('click', close);
}
@ -45,10 +45,10 @@ module.exports = function(selected, options, translate, changed) {
<polygon points="8 18 17 28 26 18" fill="#0094fb"/>
</svg>
</div>
<ul id="${id}" class="selectOptions">
<ul id="${id}" class="selectbox__options">
${options.map(
i =>
html`<li class="selectOption" onclick=${choose} data-value="${i}">${number(
html`<li class="selectbox__option" onclick=${choose} data-value="${i}">${number(
i
)}</li>`
)}

View File

@ -0,0 +1,36 @@
.selectbox {
display: inline-block;
position: relative;
cursor: pointer;
}
.selectbox__options {
display: none;
}
.selectbox__options--active {
display: block;
position: absolute;
top: 0;
left: 0;
padding: 0;
margin: 40px 0;
background-color: white;
border: 1px solid rgba(12, 12, 13, 0.3);
border-radius: 4px;
box-shadow: 1px 2px 4px rgba(12, 12, 13, 0.3);
}
.selectbox__option {
color: var(--lightTextColor);
font-size: 12pt;
list-style: none;
user-select: none;
white-space: nowrap;
padding: 0 60px;
border-bottom: 1px solid rgba(12, 12, 13, 0.3);
}
.selectbox__option:hover {
background-color: #f4f4f4;
}

View File

@ -0,0 +1,49 @@
const html = require('choo/html');
const passwordInput = require('../passwordInput');
module.exports = function(state, emit) {
const file = state.storage.getFileById(state.params.id);
const div = html`
<div class="setPasswordSection">
<div class="checkbox">
<input
class="checkbox__input"
id="add-password"
type="checkbox"
autocomplete="off"
onchange=${togglePasswordInput}/>
<label class="checkbox__label" for="add-password">
${state.translate('requirePasswordCheckbox')}
</label>
</div>
${passwordInput(
state.translate('unlockInputPlaceholder'),
state.translate('addPasswordButton'),
addPassword
)}
</div>`;
function addPassword(event) {
event.preventDefault();
const password = document.getElementById('password-input').value;
if (password.length > 0) {
emit('password', { password, file });
}
return false;
}
function togglePasswordInput(e) {
const unlockInput = document.getElementById('password-input');
const boxChecked = e.target.checked;
document
.querySelector('form.passwordInput')
.classList.toggle('passwordInput--hidden', !boxChecked);
if (boxChecked) {
unlockInput.focus();
} else {
unlockInput.value = '';
}
}
return div;
};

View File

@ -0,0 +1,56 @@
.setPasswordSection {
padding: 10px 0;
align-self: left;
max-width: 100%;
overflow-wrap: break-word;
}
.checkbox {
min-height: 24px;
}
.checkbox__input {
position: absolute;
visibility: collapse;
}
.checkbox__label {
line-height: 23px;
cursor: pointer;
color: var(--lightTextColor);
}
.checkbox__label::before {
content: '';
height: 20px;
width: 20px;
margin-right: 10px;
margin-left: 5px;
float: left;
border: 1px solid rgba(12, 12, 13, 0.3);
border-radius: 2px;
}
.checkbox:hover .checkbox__label::before {
border: 1px solid var(--primaryControlBGColor);
}
.checkbox__input:checked + .checkbox__label {
color: #000;
}
.checkbox__input:checked + .checkbox__label::before {
background-image: url('../assets/check-16-blue.svg');
background-position: 2px 1px;
}
@media (max-device-width: 520px), (max-width: 520px) {
.setPasswordSection {
align-self: center;
min-width: 95%;
}
.checkbox__label::before {
margin-left: 0;
}
}

View File

@ -1,81 +0,0 @@
const html = require('choo/html');
const raw = require('choo/html/raw');
module.exports = function(state, emit) {
const file = state.storage.getFileById(state.params.id);
return html`<div class="selectPassword">
${passwordSpan(file.password)}
<button
id="resetButton"
onclick=${toggleResetInput}
>${state.translate('changePasswordButton')}</button>
<form
id='reset-form'
class="setPassword hidden"
onsubmit=${resetPassword}
data-no-csrf>
<input id="unlock-reset-input"
class="unlock-input input-no-btn"
maxlength="32"
autocomplete="off"
type="password"
oninput=${inputChanged}
placeholder="${state.translate('unlockInputPlaceholder')}">
<input type="submit"
id="unlock-reset-btn"
class="btn btn-hidden"
value="${state.translate('changePasswordButton')}"/>
</form>
</div>`;
function passwordSpan(password) {
password = password || '●●●●●';
const span = html`<span>${raw(
state.translate('passwordResult', {
password:
'<pre class="passwordOriginal"></pre><pre class="passwordMask"></pre>'
})
)}</span>`;
const og = span.querySelector('.passwordOriginal');
const masked = span.querySelector('.passwordMask');
og.textContent = password;
masked.textContent = password.replace(/./g, '●');
return span;
}
function inputChanged() {
const resetInput = document.getElementById('unlock-reset-input');
const resetBtn = document.getElementById('unlock-reset-btn');
if (resetInput.value.length > 0) {
resetBtn.classList.remove('btn-hidden');
resetInput.classList.remove('input-no-btn');
} else {
resetBtn.classList.add('btn-hidden');
resetInput.classList.add('input-no-btn');
}
}
function resetPassword(event) {
event.preventDefault();
const password = document.querySelector('#unlock-reset-input').value;
if (password.length > 0) {
document.getElementById('copy').classList.remove('wait-password');
document.getElementById('copy-btn').disabled = false;
emit('password', { password, file });
}
return false;
}
function toggleResetInput(event) {
const form = event.target.parentElement.querySelector('form');
const input = document.getElementById('unlock-reset-input');
if (form.style.visibility === 'hidden' || form.style.visibility === '') {
form.style.visibility = 'visible';
input.focus();
} else {
form.style.visibility = 'hidden';
}
inputChanged();
}
};

View File

@ -1,70 +0,0 @@
const html = require('choo/html');
module.exports = function(state, emit) {
const file = state.storage.getFileById(state.params.id);
const div = html`
<div class="selectPassword">
<div id="addPasswordWrapper">
<input
id="addPassword"
type="checkbox"
autocomplete="off"
onchange=${togglePasswordInput}/>
<label for="addPassword">
${state.translate('requirePasswordCheckbox')}
</label>
</div>
<form class="setPassword hidden" onsubmit=${setPassword} data-no-csrf>
<input id="unlock-input"
class="unlock-input input-no-btn"
maxlength="32"
autocomplete="off"
placeholder="${state.translate('unlockInputPlaceholder')}"
type="password"
oninput=${inputChanged}/>
<input type="submit"
id="unlock-btn"
class="btn btn-hidden"
value="${state.translate('addPasswordButton')}"/>
</form>
</div>`;
function inputChanged() {
const input = document.getElementById('unlock-input');
const btn = document.getElementById('unlock-btn');
if (input.value.length > 0) {
btn.classList.remove('btn-hidden');
input.classList.remove('input-no-btn');
} else {
btn.classList.add('btn-hidden');
input.classList.add('input-no-btn');
}
}
function togglePasswordInput(e) {
const unlockInput = document.getElementById('unlock-input');
const boxChecked = e.target.checked;
document
.querySelector('.setPassword')
.classList.toggle('hidden', !boxChecked);
if (boxChecked) {
unlockInput.focus();
} else {
unlockInput.value = '';
}
inputChanged();
}
function setPassword(event) {
event.preventDefault();
const password = document.getElementById('unlock-input').value;
if (password.length > 0) {
document.getElementById('copy').classList.remove('wait-password');
document.getElementById('copy-btn').disabled = false;
emit('password', { password, file });
}
return false;
}
return div;
};

View File

@ -134,10 +134,10 @@ function delay(delay = 100) {
return new Promise(resolve => setTimeout(resolve, delay));
}
function fadeOut(id) {
const classes = document.getElementById(id).classList;
classes.remove('fadeIn');
classes.add('fadeOut');
function fadeOut(selector) {
const classes = document.querySelector(selector).classList;
classes.remove('effect--fadeIn');
classes.add('effect--fadeOut');
return delay(300);
}

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path class="icon-copy" fill="#0A8DFF" d="M14.707 8.293l-3-3A1 1 0 0 0 11 5h-1V4a1 1 0 0 0-.293-.707l-3-3A1 1 0 0 0 6 0H3a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3v3a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V9a1 1 0 0 0-.293-.707zM12.586 9H11V7.414zm-5-5H6V2.414zM6 7v2H3V2h2v2.5a.5.5 0 0 0 .5.5H8a2 2 0 0 0-2 2zm2 7V7h2v2.5a.5.5 0 0 0 .5.5H13v4z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#0A8DFF" d="M14.707 8.293l-3-3A1 1 0 0 0 11 5h-1V4a1 1 0 0 0-.293-.707l-3-3A1 1 0 0 0 6 0H3a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3v3a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V9a1 1 0 0 0-.293-.707zM12.586 9H11V7.414zm-5-5H6V2.414zM6 7v2H3V2h2v2.5a.5.5 0 0 0 .5.5H8a2 2 0 0 0-2 2zm2 7V7h2v2.5a.5.5 0 0 0 .5.5H13v4z"/></svg>

Before

Width:  |  Height:  |  Size: 416 B

After

Width:  |  Height:  |  Size: 398 B

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
last 2 chrome versions
last 2 firefox versions
firefox esr
ie >= 9
safari >= 9
safari > 9

6107
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@
"clean": "rimraf dist",
"build": "npm run clean && webpack -p",
"lint": "npm-run-all lint:*",
"lint:css": "stylelint -v",
"lint:css": "stylelint app/*.css app/**/*.css",
"lint:js": "eslint .",
"lint-locales": "node scripts/lint-locales",
"lint-locales:dev": "npm run lint-locales",
@ -51,7 +51,6 @@
"node": ">=8.2.0"
},
"devDependencies": {
"autoprefixer": "^8.0.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-plugin-yo-yoify": "^1.0.2",
@ -64,13 +63,13 @@
"cross-env": "^5.1.3",
"css-loader": "^0.28.9",
"css-mqpacker": "^6.0.2",
"cssnano": "^3.10.0",
"eslint": "^4.17.0",
"eslint-plugin-mocha": "^4.11.0",
"eslint-plugin-node": "^6.0.0",
"eslint-plugin-security": "^1.4.0",
"expose-loader": "^0.7.4",
"extract-loader": "^1.0.2",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.6",
"fluent-intl-polyfill": "^0.1.0",
"git-rev-sync": "^1.9.1",
@ -80,9 +79,12 @@
"lint-staged": "^6.1.0",
"mocha": "^5.0.0",
"nanobus": "^4.3.2",
"nanotiming": "^7.3.0",
"npm-run-all": "^4.1.2",
"nsp": "^3.1.0",
"nyc": "^11.4.1",
"nyc": "^11.5.0",
"postcss-cssnext": "^3.1.0",
"postcss-import": "^11.1.0",
"postcss-loader": "^2.1.0",
"prettier": "^1.10.2",
"proxyquire": "^1.8.0",
@ -105,7 +107,7 @@
"webpack-unassert-loader": "^1.2.0"
},
"dependencies": {
"aws-sdk": "^2.192.0",
"aws-sdk": "^2.196.0",
"body-parser": "^1.18.2",
"choo": "^6.7.0",
"cldr-core": "^32.0.0",

View File

@ -1,14 +1,12 @@
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');
const mqpacker = require('css-mqpacker');
const config = require('./server/config');
const options = {
plugins: [autoprefixer, mqpacker, cssnano]
plugins: {
'postcss-import': {},
'postcss-cssnext': {},
'css-mqpacker': {}
}
};
if (config.env === 'development') {
if (process.env.NODE_ENV === 'development') {
options.map = { inline: true };
}

View File

@ -30,7 +30,7 @@ module.exports = function(state, body = '') {
<title>${state.title}</title>
<link rel="stylesheet" type="text/css" href="${assets.get('main.css')}" />
<link rel="stylesheet" type="text/css" href="${assets.get('style.css')}" />
<!-- generic favicons -->
<link rel="icon" href="${assets.get('favicon-32.png')}" sizes="32x32">

View File

@ -2,6 +2,7 @@ const path = require('path');
const webpack = require('webpack');
const CopyPlugin = require('copy-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const IS_DEV = process.env.NODE_ENV === 'development';
const regularJSOptions = {
@ -13,7 +14,8 @@ const regularJSOptions = {
module.exports = {
entry: {
vendor: ['babel-polyfill', 'fluent'],
app: ['./app/main.js']
app: ['./app/main.js'],
style: ['./app/main.css']
},
output: {
filename: '[name].[chunkhash:8].js',
@ -88,17 +90,15 @@ module.exports = {
},
{
test: /\.css$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[hash:8].[ext]'
}
},
'extract-loader',
{ loader: 'css-loader', options: { importLoaders: 1 } },
'postcss-loader'
]
use: ExtractTextPlugin.extract({
use: [
{
loader: 'css-loader',
options: { modules: false, importLoaders: 1 }
},
'postcss-loader'
]
})
},
{
test: require.resolve('./package.json'),
@ -153,6 +153,7 @@ module.exports = {
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
}),
new ExtractTextPlugin('style.[chunkhash:8].css'),
new ManifestPlugin()
],
devServer: {