From f0cfc19f8c904aec9cba0bbfbef2c99f8970d5aa Mon Sep 17 00:00:00 2001
From: Danny Coates
Date: Wed, 24 Oct 2018 19:07:10 -0700
Subject: [PATCH] a new approach for the ui
---
.stylelintrc | 1 +
app/archive.js | 7 +-
app/dragManager.js | 10 +-
app/fileManager.js | 38 +-
app/main.css | 109 ++--
app/main.js | 9 +
app/routes.js | 55 ++
app/routes/download.js | 30 --
app/routes/index.js | 65 ---
app/ui/account.js | 63 +++
app/ui/archiveList.js | 41 ++
app/ui/archiveTile.js | 260 ++++++++++
app/ui/blank.js | 5 +
app/ui/download.js | 103 ++++
app/ui/error.js | 18 +
app/ui/expiryOptions.js | 73 +++
app/ui/footer.js | 52 ++
app/ui/fxPromo.js | 19 +
app/ui/header.js | 21 +
app/ui/legal.js | 33 ++
app/ui/modal.js | 15 +
app/ui/notFound.js | 18 +
app/ui/selectbox.js | 25 +
app/ui/signupDialog.js | 56 +++
app/ui/unsupported.js | 65 +++
app/ui/welcome.js | 6 +
app/utils.js | 48 +-
package-lock.json | 206 ++++++++
package.json | 1 +
postcss.config.js | 1 +
public/locales/en-US/send.ftl | 4 +
server/fxa.js | 5 +-
server/routes/jsconfig.js | 10 +-
tailwind.js | 920 ++++++++++++++++++++++++++++++++++
34 files changed, 2246 insertions(+), 146 deletions(-)
create mode 100644 app/routes.js
delete mode 100644 app/routes/download.js
delete mode 100644 app/routes/index.js
create mode 100644 app/ui/account.js
create mode 100644 app/ui/archiveList.js
create mode 100644 app/ui/archiveTile.js
create mode 100644 app/ui/blank.js
create mode 100644 app/ui/download.js
create mode 100644 app/ui/error.js
create mode 100644 app/ui/expiryOptions.js
create mode 100644 app/ui/footer.js
create mode 100644 app/ui/fxPromo.js
create mode 100644 app/ui/header.js
create mode 100644 app/ui/legal.js
create mode 100644 app/ui/modal.js
create mode 100644 app/ui/notFound.js
create mode 100644 app/ui/selectbox.js
create mode 100644 app/ui/signupDialog.js
create mode 100644 app/ui/unsupported.js
create mode 100644 app/ui/welcome.js
create mode 100644 tailwind.js
diff --git a/.stylelintrc b/.stylelintrc
index 9ade6144..0af67b03 100644
--- a/.stylelintrc
+++ b/.stylelintrc
@@ -10,3 +10,4 @@ rules:
declaration-colon-newline-after: null
selector-list-comma-newline-after: null
value-list-comma-newline-after: null
+ at-rule-no-unknown: null
diff --git a/app/archive.js b/app/archive.js
index 6cdce5f4..dea25cd1 100644
--- a/app/archive.js
+++ b/app/archive.js
@@ -62,7 +62,10 @@ export default class Archive {
return true;
}
- remove(index) {
- this.files.splice(index, 1);
+ remove(file) {
+ const index = this.files.indexOf(file);
+ if (index > -1) {
+ this.files.splice(index, 1);
+ }
}
}
diff --git a/app/dragManager.js b/app/dragManager.js
index 3002e9c1..9c42426a 100644
--- a/app/dragManager.js
+++ b/app/dragManager.js
@@ -3,16 +3,16 @@ export default function(state, emitter) {
document.body.addEventListener('dragover', event => {
if (state.route === '/') {
event.preventDefault();
- const files = document.querySelector('.uploadedFilesWrapper');
- files.classList.add('uploadArea--noEvents');
+ // const files = document.querySelector('.uploadedFilesWrapper');
+ // files.classList.add('uploadArea--noEvents');
}
});
document.body.addEventListener('drop', event => {
if (state.route === '/' && !state.uploading) {
event.preventDefault();
- document
- .querySelector('.uploadArea')
- .classList.remove('uploadArea--dragging');
+ // document
+ // .querySelector('.uploadArea')
+ // .classList.remove('uploadArea--dragging');
const files = Array.from(event.dataTransfer.files);
diff --git a/app/fileManager.js b/app/fileManager.js
index bd45b252..7e2eff00 100644
--- a/app/fileManager.js
+++ b/app/fileManager.js
@@ -66,8 +66,11 @@ export default function(state, emitter) {
metrics.changedDownloadLimit(file);
});
- emitter.on('removeUpload', async ({ index }) => {
- state.archive.remove(index);
+ emitter.on('removeUpload', file => {
+ state.archive.remove(file);
+ if (state.archive.numFiles === 0) {
+ state.archive = null;
+ }
render();
});
@@ -86,6 +89,7 @@ export default function(state, emitter) {
} catch (e) {
state.raven.captureException(e);
}
+ render();
});
emitter.on('cancel', () => {
@@ -149,15 +153,26 @@ export default function(state, emitter) {
if (password) {
emitter.emit('password', { password, file: ownedFile });
}
-
- const cancelBtn = document.getElementById('cancel-upload');
- if (cancelBtn) {
- cancelBtn.hidden = 'hidden';
- }
- if (document.querySelector('.page')) {
- await delay(1000);
- }
- emitter.emit('pushState', `/share/${ownedFile.id}`);
+ state.animation = () => {
+ const x = document.querySelector('.foo');
+ const y = x.previousElementSibling;
+ x.animate(
+ [
+ { transform: `translateY(-${y.getBoundingClientRect().height}px)` },
+ { transform: 'translateY(0)' }
+ ],
+ {
+ duration: 400,
+ easing: 'ease'
+ }
+ );
+ y.animate([{ opacity: 0 }, { opacity: 1 }], {
+ delay: 300,
+ duration: 100,
+ fill: 'both'
+ });
+ };
+ // emitter.emit('pushState', `/share/${ownedFile.id}`);
} catch (err) {
if (err.message === '0') {
//cancelled. do nothing
@@ -176,6 +191,7 @@ export default function(state, emitter) {
state.password = '';
state.uploading = false;
state.transfer = null;
+ render();
}
});
diff --git a/app/main.css b/app/main.css
index a0c0661b..e3c7fdc1 100644
--- a/app/main.css
+++ b/app/main.css
@@ -1,28 +1,81 @@
-@import './base.css';
-@import './pages/share/share.css';
-@import './pages/signin/signin.css';
-@import './pages/uploads/uploads.css';
-@import './pages/unsupported/unsupported.css';
-@import './templates/archiveTile/archiveTile.css';
-@import './templates/controlArea/controlArea.css';
-@import './templates/downloadButton/downloadButton.css';
-@import './templates/downloadPassword/downloadPassword.css';
-@import './templates/file/file.css';
-@import './templates/fileIcon/fileIcon.css';
-@import './templates/fileList/fileList.css';
-@import './templates/fileManager/fileManager.css';
-@import './templates/footer/footer.css';
-@import './templates/fxPromo/fxPromo.css';
-@import './templates/header/header.css';
-@import './templates/modal/modal.css';
-@import './templates/okDialog/okDialog.css';
-@import './templates/passwordInput/passwordInput.css';
-@import './templates/popup/popup.css';
-@import './templates/selectbox/selectbox.css';
-@import './templates/setPasswordSection/setPasswordSection.css';
-@import './templates/signupDialog/signupDialog.css';
-@import './templates/signupPromo/signupPromo.css';
-@import './templates/title/title.css';
-@import './templates/uploadedFile/uploadedFile.css';
-@import './templates/uploadedFileList/uploadedFileList.css';
-@import './templates/userAccount/userAccount.css';
+@import 'tailwindcss/preflight';
+@import 'tailwindcss/components';
+@import 'tailwindcss/utilities';
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+progress {
+ @apply bg-grey-light;
+ @apply rounded-sm;
+ @apply w-full;
+ @apply h-1;
+}
+
+progress::-moz-progress-bar {
+ @apply bg-blue;
+ @apply rounded-sm;
+}
+
+progress::-webkit-progress-bar {
+ @apply bg-grey-light;
+ @apply rounded-sm;
+ @apply w-full;
+ @apply h-1;
+}
+
+progress::-webkit-progress-value {
+ @apply bg-blue;
+ @apply rounded-sm;
+}
+
+.main {
+ @apply bg-blue-lightest;
+
+ min-height: calc(100vh - 6rem);
+}
+
+.header-logo {
+ background-image: url('../assets/send_logo.svg');
+ background-position: left;
+ background-repeat: no-repeat;
+ background-size: 2rem;
+ padding-left: 2.5rem;
+ text-decoration: none;
+}
+
+.feedback-link {
+ background-color: #000;
+ background-image: url('../assets/feedback.svg');
+ background-position: 0.125rem 0.25rem;
+ background-repeat: no-repeat;
+ background-size: 1.125rem;
+ color: #fff;
+ display: block;
+ font-size: 0.75rem;
+ line-height: 0.75rem;
+ padding: 0.375rem 0.375rem 0.375rem 1.25rem;
+ text-indent: 0.125rem;
+ white-space: nowrap;
+}
+
+.bg-shades {
+ background-color: rgba(0, 0, 0, 0.7);
+}
+
+@screen md {
+ .main {
+ @apply flex-1;
+ @apply self-center;
+ @apply bg-white;
+ @apply shadow-md;
+ @apply m-auto;
+
+ min-width: 30rem;
+ max-width: 60rem;
+ min-height: 30rem;
+ max-height: 38rem;
+ }
+}
diff --git a/app/main.js b/app/main.js
index 8febde65..0d8df162 100644
--- a/app/main.js
+++ b/app/main.js
@@ -34,6 +34,15 @@ import User from './user';
window.appState = state;
window.appEmit = emitter.emit.bind(emitter);
let unsupportedReason = null;
+
+ emitter.on('render', () => {
+ if (state.animation) {
+ window.requestAnimationFrame(() => {
+ state.animation();
+ state.animation = null;
+ });
+ }
+ });
if (
// Firefox < 50
/firefox/i.test(navigator.userAgent) &&
diff --git a/app/routes.js b/app/routes.js
new file mode 100644
index 00000000..f36199d6
--- /dev/null
+++ b/app/routes.js
@@ -0,0 +1,55 @@
+const choo = require('choo');
+const html = require('choo/html');
+const nanotiming = require('nanotiming');
+const download = require('./ui/download');
+const footer = require('./ui/footer');
+const fxPromo = require('./ui/fxPromo');
+const modal = require('./ui/modal');
+const header = require('./ui/header');
+
+nanotiming.disabled = true;
+
+function banner(state, emit) {
+ if (state.promo && !state.route.startsWith('/unsupported/')) {
+ return fxPromo(state, emit);
+ }
+}
+
+function body(main) {
+ return function(state, emit) {
+ const b = html`
+ ${state.modal && modal(state, emit)}
+ ${banner(state, emit)}
+ ${header(state, emit)}
+ ${main(state, emit)}
+ ${footer(state)}
+ `;
+ if (state.layout) {
+ // server side only
+ return state.layout(state, b);
+ }
+ return b;
+ };
+}
+
+module.exports = function() {
+ const app = choo();
+ app.route('/', body(require('./ui/welcome')));
+ app.route('/download/:id', body(download));
+ app.route('/download/:id/:key', body(download));
+ app.route('/unsupported/:reason', body(require('./ui/unsupported')));
+ app.route('/legal', body(require('./ui/legal')));
+ app.route('/error', body(require('./ui/error')));
+ app.route('/blank', body(require('./ui/blank')));
+ app.route('/oauth', async function(state, emit) {
+ try {
+ await state.user.finishLogin(state.query.code, state.query.state);
+ emit('replaceState', '/');
+ } catch (e) {
+ emit('replaceState', '/error');
+ setTimeout(() => emit('render'));
+ }
+ });
+ app.route('*', body(require('./ui/notFound')));
+ return app;
+};
diff --git a/app/routes/download.js b/app/routes/download.js
deleted file mode 100644
index 722a6b45..00000000
--- a/app/routes/download.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/* global downloadMetadata */
-const preview = require('../pages/preview');
-const password = require('../pages/password');
-
-function createFileInfo(state) {
- return {
- id: state.params.id,
- secretKey: state.params.key,
- nonce: downloadMetadata.nonce,
- requiresPassword: downloadMetadata.pwd
- };
-}
-
-module.exports = function(state, emit) {
- if (!state.fileInfo) {
- state.fileInfo = createFileInfo(state);
- }
-
- if (!state.transfer && !state.fileInfo.requiresPassword) {
- emit('getMetadata');
- }
-
- if (state.transfer) {
- return preview(state, emit);
- }
-
- if (state.fileInfo.requiresPassword && !state.fileInfo.password) {
- return password(state, emit);
- }
-};
diff --git a/app/routes/index.js b/app/routes/index.js
deleted file mode 100644
index 80ff1a04..00000000
--- a/app/routes/index.js
+++ /dev/null
@@ -1,65 +0,0 @@
-const choo = require('choo');
-const html = require('choo/html');
-const nanotiming = require('nanotiming');
-const download = require('./download');
-const footer = require('../templates/footer');
-const fxPromo = require('../templates/fxPromo');
-const modal = require('../templates/modal');
-const header = require('../templates/header');
-
-nanotiming.disabled = true;
-
-module.exports = function() {
- const app = choo();
-
- function banner(state, emit) {
- if (state.promo && !state.route.startsWith('/unsupported/')) {
- return fxPromo(state, emit);
- }
- }
-
- function modalDialog(state, emit) {
- if (state.modal) {
- return modal(state, emit);
- }
- }
-
- function body(page) {
- return function(state, emit) {
- const b = html`
- ${modalDialog(state, emit)}
- ${banner(state, emit)}
- ${header(state, emit)}
- ${page(state, emit)}
- ${footer(state)}
- `;
- if (state.layout) {
- // server side only
- return state.layout(state, b);
- }
- return b;
- };
- }
-
- app.route('/', body(require('../pages/welcome')));
- app.route('/share/:id', body(require('../pages/share')));
- app.route('/uploads', body(require('../pages/uploads')));
- app.route('/download/:id', body(download));
- app.route('/download/:id/:key', body(download));
- app.route('/unsupported/:reason', body(require('../pages/unsupported')));
- app.route('/legal', body(require('../pages/legal')));
- app.route('/error', body(require('../pages/error')));
- app.route('/blank', body(require('../pages/blank')));
- app.route('/signin', body(require('../pages/signin')));
- app.route('/oauth', async function(state, emit) {
- try {
- await state.user.finishLogin(state.query.code, state.query.state);
- emit('replaceState', '/');
- } catch (e) {
- emit('replaceState', '/error');
- setTimeout(() => emit('render'));
- }
- });
- app.route('*', body(require('../pages/notFound')));
- return app;
-};
diff --git a/app/ui/account.js b/app/ui/account.js
new file mode 100644
index 00000000..5908144a
--- /dev/null
+++ b/app/ui/account.js
@@ -0,0 +1,63 @@
+const html = require('choo/html');
+const itemClass =
+ 'block p-2 text-grey-darkest hover:bg-blue hover:text-white cursor-pointer';
+
+module.exports = function(state, emit) {
+ if (!state.capabilities.account) {
+ return null;
+ }
+ const user = state.user;
+ const menuItems = [];
+ if (user.loggedIn) {
+ menuItems.push(html`${user.email}`);
+ menuItems.push(
+ html`${state.translate(
+ 'logOut'
+ )}`
+ );
+ } else {
+ menuItems.push(
+ html`${state.translate(
+ 'signInMenuOption'
+ )}`
+ );
+ }
+ return html``;
+
+ function avatarClick(event) {
+ event.preventDefault();
+ const menu = document.getElementById('accountMenu');
+ menu.classList.toggle('invisible');
+ menu.focus();
+ }
+
+ function hideMenu(event) {
+ event.stopPropagation();
+ const menu = document.getElementById('accountMenu');
+ menu.classList.add('invisible');
+ }
+
+ function login(event) {
+ event.preventDefault();
+ emit('login');
+ }
+
+ function logout(event) {
+ event.preventDefault();
+ emit('logout');
+ }
+};
diff --git a/app/ui/archiveList.js b/app/ui/archiveList.js
new file mode 100644
index 00000000..3754fffc
--- /dev/null
+++ b/app/ui/archiveList.js
@@ -0,0 +1,41 @@
+const html = require('choo/html');
+const assets = require('../../common/assets');
+const { list } = require('../utils');
+const archiveTile = require('./archiveTile');
+
+function intro(state) {
+ return html`
+
+
+
${state.translate('uploadPageHeader')}
+ ${state.translate('pageHeaderCredits')}
+
+
+ ${state.translate(
+ 'uploadPageExplainer'
+ )}
+ `;
+}
+
+module.exports = function(state, emit) {
+ const archives = state.storage.files.map(archive =>
+ archiveTile(state, emit, archive)
+ );
+ let wip = '';
+ if (state.uploading) {
+ wip = archiveTile.uploading(state, emit);
+ } else if (state.archive) {
+ wip = archiveTile.wip(state, emit);
+ } else {
+ wip = archiveTile.empty(state, emit);
+ }
+ archives.reverse();
+ if (archives.length < 1) {
+ archives.push(intro(state));
+ }
+ return html`
+
+ ${wip}
+ ${list(archives, 'list-reset h-full overflow-y-scroll foo', 'py-2')}
+ `;
+};
diff --git a/app/ui/archiveTile.js b/app/ui/archiveTile.js
new file mode 100644
index 00000000..25ac3a23
--- /dev/null
+++ b/app/ui/archiveTile.js
@@ -0,0 +1,260 @@
+const html = require('choo/html');
+const raw = require('choo/html/raw');
+const assets = require('../../common/assets');
+const { bytes, copyToClipboard, list, percent, timeLeft } = require('../utils');
+const expiryOptions = require('./expiryOptions');
+
+function expiryInfo(translate, archive) {
+ const l10n = timeLeft(archive.expiresAt - Date.now());
+ return raw(
+ translate('frontPageExpireInfo', {
+ downloadCount: translate('downloadCount', {
+ num: archive.dlimit - archive.dtotal
+ }),
+ timespan: translate(l10n.id, l10n)
+ })
+ );
+}
+
+function fileInfo(file, action) {
+ return html`
+
+
+
+
${file.name}
+ ${bytes(file.size)}
+ ${file.type}
+
+ ${action}
+ `;
+}
+
+function archiveDetails(translate, archive) {
+ if (archive.manifest.files.length > 1) {
+ return html`
+
+ ${translate('fileCount', {
+ num: archive.manifest.files.length
+ })}
+ ${list(archive.manifest.files.map(f => fileInfo(f)), 'list-reset')}
+ `;
+ }
+}
+
+module.exports = function(state, emit, archive) {
+ return html`
+
+
+
+
+
${archive.name}
+ ${bytes(archive.size)}
+
+
+ ${expiryInfo(state.translate, archive)}
+
+ ${archiveDetails(state.translate, archive)}
+
+
+ `;
+
+ function copy(event) {
+ event.stopPropagation();
+ copyToClipboard(archive.url);
+ }
+
+ function del(event) {
+ event.stopPropagation();
+ emit('delete', { file: archive, location: 'success-screen' });
+ }
+};
+
+module.exports.wip = function(state, emit) {
+ return html`
+
+ ${list(state.archive.files.map(f => fileInfo(f, remove(f))), 'list-reset')}
+
+
+
+
+ ${expiryOptions(state, emit)}
+
+ `;
+
+ function upload(event) {
+ event.preventDefault();
+ event.target.disabled = true;
+ if (!state.uploading) {
+ emit('upload', {
+ type: 'click',
+ dlimit: state.downloadCount || 1,
+ password: state.password
+ });
+ }
+ }
+
+ function add(event) {
+ event.preventDefault();
+ const newFiles = Array.from(event.target.files);
+
+ emit('addFiles', { files: newFiles });
+ }
+
+ function remove(file) {
+ return html`
+ `;
+ function del(event) {
+ event.stopPropagation();
+ emit('removeUpload', file);
+ }
+ }
+};
+
+module.exports.uploading = function(state, emit) {
+ const progress = state.transfer.progressRatio;
+ const progressPercent = percent(progress);
+ const archive = state.archive;
+ return html`
+
+
+
+
${archive.name}
+ ${bytes(archive.size)}
+
+
+ ${expiryInfo(state.translate, {
+ dlimit: state.downloadCount || 1,
+ dtotal: 0,
+ expiresAt: Date.now() + 500 + state.timeLimit * 1000
+ })}
+
+ ${progressPercent}
+
+
+ `;
+
+ function cancel(event) {
+ event.stopPropagation();
+ event.target.disabled = true;
+ emit('cancel');
+ }
+};
+
+module.exports.empty = function(state, emit) {
+ return html`
+
+ ${state.translate('uploadDropDragMessage')}
+
+
+ `;
+
+ function add(event) {
+ event.preventDefault();
+ const newFiles = Array.from(event.target.files);
+
+ emit('addFiles', { files: newFiles });
+ }
+};
+
+module.exports.preview = function(state, emit) {
+ const archive = state.fileInfo;
+ return html`
+
+
+
+
${archive.name}
+ ${bytes(archive.size)}
+
+ ${archiveDetails(state.translate, archive)}
+
+
+ `;
+
+ function download(event) {
+ event.preventDefault();
+ event.target.disabled = true;
+ emit('download', archive);
+ }
+};
+
+module.exports.downloading = function(state, emit) {
+ const archive = state.fileInfo;
+ const progress = state.transfer.progressRatio;
+ const progressPercent = percent(progress);
+ return html`
+
+
+
+
${archive.name}
+ ${bytes(archive.size)}
+
+ ${progressPercent}
+
+
+ `;
+
+ function cancel(event) {
+ event.preventDefault();
+ event.target.disabled = true;
+ emit('download', archive);
+ }
+};
diff --git a/app/ui/blank.js b/app/ui/blank.js
new file mode 100644
index 00000000..6e8decdc
--- /dev/null
+++ b/app/ui/blank.js
@@ -0,0 +1,5 @@
+const html = require('choo/html');
+
+module.exports = function() {
+ return html``;
+};
diff --git a/app/ui/download.js b/app/ui/download.js
new file mode 100644
index 00000000..bebd2b60
--- /dev/null
+++ b/app/ui/download.js
@@ -0,0 +1,103 @@
+/* global downloadMetadata */
+const html = require('choo/html');
+const archiveTile = require('./archiveTile');
+
+function password(state, emit) {
+ const fileInfo = state.fileInfo;
+ const invalid = fileInfo.password === null;
+
+ const visible = invalid ? 'visible' : 'invisible';
+ const invalidBtn = invalid ? '' : '';
+
+ const div = html`
+
+
+
+
+
`;
+
+ if (!(div instanceof String)) {
+ setTimeout(() => document.getElementById('password-input').focus());
+ }
+
+ function inputChanged() {
+ //TODO
+ const input = document.querySelector('.passwordForm__error');
+ input.classList.remove('visible');
+ const btn = document.getElementById('password-btn');
+ btn.classList.remove('unlockBtn--error');
+ }
+
+ function checkPassword(event) {
+ event.preventDefault();
+ const password = document.getElementById('password-input').value;
+ if (password.length > 0) {
+ document.getElementById('password-btn').disabled = true;
+ state.fileInfo.url = window.location.href;
+ state.fileInfo.password = password;
+ emit('getMetadata');
+ }
+ return false;
+ }
+
+ return div;
+}
+
+function createFileInfo(state) {
+ return {
+ id: state.params.id,
+ secretKey: state.params.key,
+ nonce: downloadMetadata.nonce,
+ requiresPassword: downloadMetadata.pwd
+ };
+}
+
+module.exports = function(state, emit) {
+ let content = '';
+ if (!state.fileInfo) {
+ state.fileInfo = createFileInfo(state);
+ }
+
+ if (!state.transfer && !state.fileInfo.requiresPassword) {
+ emit('getMetadata');
+ }
+
+ if (state.transfer) {
+ switch (state.transfer.state) {
+ case 'downloading':
+ case 'decrypting':
+ content = archiveTile.downloading(state, emit);
+ break;
+ case 'complete':
+ content = ''; //TODO
+ break;
+ default:
+ content = archiveTile.preview(state, emit);
+ }
+ } else if (state.fileInfo.requiresPassword && !state.fileInfo.password) {
+ content = password(state, emit);
+ }
+ return html`
+
+
+ `;
+};
diff --git a/app/ui/error.js b/app/ui/error.js
new file mode 100644
index 00000000..f9f8e4ad
--- /dev/null
+++ b/app/ui/error.js
@@ -0,0 +1,18 @@
+const html = require('choo/html');
+const assets = require('../../common/assets');
+
+module.exports = function(state) {
+ return html`
+
+
+
${state.translate('errorPageHeader')}
+
+
+ ${state.translate('uploadPageExplainer')}
+
+
+ ${state.translate('sendYourFilesLink')}
+
+
+ `;
+};
diff --git a/app/ui/expiryOptions.js b/app/ui/expiryOptions.js
new file mode 100644
index 00000000..b94ffe7b
--- /dev/null
+++ b/app/ui/expiryOptions.js
@@ -0,0 +1,73 @@
+/* globals DEFAULTS */
+const html = require('choo/html');
+const raw = require('choo/html/raw');
+const { secondsToL10nId } = require('../utils');
+const selectbox = require('./selectbox');
+const signupDialog = require('./signupDialog');
+
+module.exports = function(state, emit) {
+ const el = html`
+
+ ${raw(
+ state.translate('frontPageExpireInfo', {
+ downloadCount: '',
+ timespan: ''
+ })
+ )}
+
`;
+ if (el.__encoded) {
+ // we're rendering on the server
+ return el;
+ }
+
+ const counts = DEFAULTS.DOWNLOAD_COUNTS.filter(
+ i => state.capabilities.account || i <= state.user.maxDownloads
+ );
+
+ const dlCountSelect = el.querySelector('#dlCount');
+ el.replaceChild(
+ selectbox(
+ state.downloadCount || 1,
+ counts,
+ num => state.translate('downloadCount', { num }),
+ value => {
+ const max = state.user.maxDownloads;
+ if (value > max) {
+ state.modal = signupDialog();
+ value = max;
+ }
+ state.downloadCount = value;
+ emit('render');
+ }
+ ),
+ dlCountSelect
+ );
+
+ const expires = DEFAULTS.EXPIRE_TIMES_SECONDS.filter(
+ i => state.capabilities.account || i <= state.user.maxExpireSeconds
+ );
+
+ const timeSelect = el.querySelector('#timespan');
+ el.replaceChild(
+ selectbox(
+ state.timeLimit || 86400,
+ expires,
+ num => {
+ const l10n = secondsToL10nId(num);
+ return state.translate(l10n.id, l10n);
+ },
+ value => {
+ const max = state.user.maxExpireSeconds;
+ if (value > max) {
+ state.modal = signupDialog();
+ value = max;
+ }
+ state.timeLimit = value;
+ emit('render');
+ }
+ ),
+ timeSelect
+ );
+
+ return el;
+};
diff --git a/app/ui/footer.js b/app/ui/footer.js
new file mode 100644
index 00000000..90f27b6e
--- /dev/null
+++ b/app/ui/footer.js
@@ -0,0 +1,52 @@
+const html = require('choo/html');
+const version = require('../../package.json').version;
+const { browserName } = require('../utils');
+
+module.exports = function(state) {
+ const browser = browserName();
+ const feedbackUrl = `https://qsurvey.mozilla.com/s3/txp-firefox-send?ver=${version}&browser=${browser}`;
+ const footer = html``;
+ // HACK
+ // We only want to render this once because we
+ // toggle the targets of the links with utils/openLinksInNewTab
+ footer.isSameNode = function(target) {
+ return target && target.nodeName && target.nodeName === 'FOOTER';
+ };
+ return footer;
+};
diff --git a/app/ui/fxPromo.js b/app/ui/fxPromo.js
new file mode 100644
index 00000000..2dac4761
--- /dev/null
+++ b/app/ui/fxPromo.js
@@ -0,0 +1,19 @@
+const html = require('choo/html');
+const assets = require('../../common/assets');
+
+module.exports = function() {
+ return html`
+ `;
+};
diff --git a/app/ui/header.js b/app/ui/header.js
new file mode 100644
index 00000000..c5f898e9
--- /dev/null
+++ b/app/ui/header.js
@@ -0,0 +1,21 @@
+const html = require('choo/html');
+const account = require('./account');
+
+module.exports = function(state, emit) {
+ const header = html`
+
+
+ ${account(state, emit)}
+ `;
+ // HACK
+ // We only want to render this once because we
+ // toggle the targets of the links with utils/openLinksInNewTab
+ // header.isSameNode = function(target) {
+ // return target && target.nodeName && target.nodeName === 'HEADER';
+ // };
+ return header;
+};
diff --git a/app/ui/legal.js b/app/ui/legal.js
new file mode 100644
index 00000000..2c753e6f
--- /dev/null
+++ b/app/ui/legal.js
@@ -0,0 +1,33 @@
+const html = require('choo/html');
+const raw = require('choo/html/raw');
+
+module.exports = function(state) {
+ return html`
+
+
+
${state.translate('legalHeader')}
+ ${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/'
+ ])
+ )}
+
+ `;
+};
+
+function replaceLinks(str, urls) {
+ let i = 0;
+ const s = str.replace(
+ /([^<]+)<\/a>/g,
+ (m, v) => `${v}`
+ );
+ return `${s}
`;
+}
diff --git a/app/ui/modal.js b/app/ui/modal.js
new file mode 100644
index 00000000..e596cc5c
--- /dev/null
+++ b/app/ui/modal.js
@@ -0,0 +1,15 @@
+const html = require('choo/html');
+
+module.exports = function(state, emit) {
+ return html`
+
+
e.stopPropagation()}>
+ ${state.modal(state, emit, close)}
+
+
`;
+
+ function close(event) {
+ state.modal = null;
+ emit('render');
+ }
+};
diff --git a/app/ui/notFound.js b/app/ui/notFound.js
new file mode 100644
index 00000000..6026da35
--- /dev/null
+++ b/app/ui/notFound.js
@@ -0,0 +1,18 @@
+const html = require('choo/html');
+const assets = require('../../common/assets');
+
+module.exports = function(state) {
+ return html`
+
+
+
${state.translate('expiredPageHeader')}
+
+
+ ${state.translate('uploadPageExplainer')}
+
+
+ ${state.translate('sendYourFilesLink')}
+
+
+ `;
+};
diff --git a/app/ui/selectbox.js b/app/ui/selectbox.js
new file mode 100644
index 00000000..d90039af
--- /dev/null
+++ b/app/ui/selectbox.js
@@ -0,0 +1,25 @@
+const html = require('choo/html');
+
+module.exports = function(selected, options, translate, changed) {
+ let x = selected;
+
+ return html`
+ `;
+
+ function choose(event) {
+ const target = event.target;
+ const value = +target.value;
+
+ if (x !== value) {
+ x = value;
+ changed(value);
+ }
+ }
+};
diff --git a/app/ui/signupDialog.js b/app/ui/signupDialog.js
new file mode 100644
index 00000000..42b8a2b1
--- /dev/null
+++ b/app/ui/signupDialog.js
@@ -0,0 +1,56 @@
+/* global LIMITS */
+const html = require('choo/html');
+const { bytes } = require('../utils');
+
+module.exports = function() {
+ return function(state, emit, close) {
+ return html`
+
+
+ ${state.translate('accountBenefitTitle')}
+
+ - ${state.translate('accountBenefitLargeFiles', {
+ size: bytes(LIMITS.MAX_FILE_SIZE)
+ })}
+ - ${state.translate('accountBenefitExpiry')}
+ - ${state.translate('accountBenefitSync')}
+
+
+
+
+
+
`;
+
+ function submitEmail(event) {
+ event.preventDefault();
+ const el = document.getElementById('email-input');
+ const email = el.value;
+ if (email) {
+ // just check if it's the right shape
+ const a = email.split('@');
+ if (a.length === 2 && a.every(s => s.length > 0)) {
+ return emit('login', email);
+ }
+ }
+ el.value = '';
+ }
+ };
+};
diff --git a/app/ui/unsupported.js b/app/ui/unsupported.js
new file mode 100644
index 00000000..1a6099d6
--- /dev/null
+++ b/app/ui/unsupported.js
@@ -0,0 +1,65 @@
+const html = require('choo/html');
+const assets = require('../../common/assets');
+
+module.exports = function(state) {
+ let strings = {};
+ let why = '';
+ let url = '';
+ let buttonAction = '';
+
+ if (state.params.reason !== 'outdated') {
+ strings = unsupportedStrings(state);
+ why = html`
+
+ ${state.translate('notSupportedLink')}
+ `;
+ url =
+ 'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com';
+ buttonAction = html`
+
+ Firefox
${strings.button}
+
`;
+ } else {
+ strings = outdatedStrings(state);
+ url = 'https://support.mozilla.org/kb/update-firefox-latest-version';
+ buttonAction = html`
+
+ ${strings.button}
+
`;
+ }
+
+ return html`
+
+
+ `;
+};
+
+function outdatedStrings(state) {
+ return {
+ header: state.translate('notSupportedHeader'),
+ description: state.translate('notSupportedOutdatedDetail'),
+ button: state.translate('updateFirefox')
+ };
+}
+
+function unsupportedStrings(state) {
+ return {
+ header: state.translate('notSupportedHeader'),
+ description: state.translate('notSupportedDetail'),
+ button: state.translate('downloadFirefoxButtonSub')
+ };
+}
diff --git a/app/ui/welcome.js b/app/ui/welcome.js
new file mode 100644
index 00000000..4d10ba29
--- /dev/null
+++ b/app/ui/welcome.js
@@ -0,0 +1,6 @@
+const html = require('choo/html');
+const archiveList = require('./archiveList');
+
+module.exports = function(state, emit) {
+ return html`${archiveList(state, emit)}`;
+};
diff --git a/app/utils.js b/app/utils.js
index 51731287..761477ca 100644
--- a/app/utils.js
+++ b/app/utils.js
@@ -1,3 +1,4 @@
+const html = require('choo/html');
const b64 = require('base64-js');
function arrayToB64(array) {
@@ -182,6 +183,48 @@ async function streamToArrayBuffer(stream, size) {
return result.buffer;
}
+function list(items, ulStyle = '', liStyle = '') {
+ const lis = items.map(i => html`${i}`);
+ return html``;
+}
+
+function secondsToL10nId(seconds) {
+ if (seconds < 3600) {
+ return { id: 'timespanMinutes', num: Math.floor(seconds / 60) };
+ } else if (seconds < 86400) {
+ return { id: 'timespanHours', num: Math.floor(seconds / 3600) };
+ } else {
+ return { id: 'timespanDays', num: Math.floor(seconds / 86400) };
+ }
+}
+
+function timeLeft(milliseconds) {
+ const minutes = Math.floor(milliseconds / 1000 / 60);
+ const hours = Math.floor(minutes / 60);
+ const days = Math.floor(hours / 24);
+ if (days >= 1) {
+ return {
+ id: 'expiresDaysHoursMinutes',
+ days,
+ hours: hours % 24,
+ minutes: minutes % 60
+ };
+ }
+ if (hours >= 1) {
+ return {
+ id: 'expiresHoursMinutes',
+ hours,
+ minutes: minutes % 60
+ };
+ } else if (hours === 0) {
+ if (minutes === 0) {
+ return { id: 'expiresMinutes', minutes: '< 1' };
+ }
+ return { id: 'expiresMinutes', minutes };
+ }
+ return null;
+}
+
module.exports = {
fadeOut,
delay,
@@ -196,5 +239,8 @@ module.exports = {
isFile,
openLinksInNewTab,
browserName,
- streamToArrayBuffer
+ streamToArrayBuffer,
+ list,
+ secondsToL10nId,
+ timeLeft
};
diff --git a/package-lock.json b/package-lock.json
index cc8bbc04..66880058 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2339,6 +2339,12 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
"integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0="
},
+ "camelcase-css": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-1.0.1.tgz",
+ "integrity": "sha1-FXxCOCZfXPlKHf/ehkRlUsvz9wU=",
+ "dev": true
+ },
"camelcase-keys": {
"version": "2.1.0",
"resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
@@ -2762,6 +2768,12 @@
"integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
"dev": true
},
+ "comment-regex": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/comment-regex/-/comment-regex-1.0.1.tgz",
+ "integrity": "sha512-IWlN//Yfby92tOIje7J18HkNmWRR7JESA/BK8W7wqY/akITpU5B0JQWnbTjCfdChSrDNb0DrdA9jfAxiiBXyiQ==",
+ "dev": true
+ },
"commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -3469,6 +3481,12 @@
"integrity": "sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ==",
"dev": true
},
+ "css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=",
+ "dev": true
+ },
"cssesc": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz",
@@ -3659,6 +3677,12 @@
}
}
},
+ "defined": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
+ "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
+ "dev": true
+ },
"del": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz",
@@ -6167,6 +6191,12 @@
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
"dev": true
},
+ "gather-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/gather-stream/-/gather-stream-1.0.0.tgz",
+ "integrity": "sha1-szmUr0V6gRVwDUEPMXczy+egkEs=",
+ "dev": true
+ },
"gaze": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
@@ -7757,6 +7787,12 @@
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
"integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc="
},
+ "js-base64": {
+ "version": "2.4.9",
+ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.9.tgz",
+ "integrity": "sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ==",
+ "dev": true
+ },
"js-levenshtein": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.4.tgz",
@@ -11290,6 +11326,62 @@
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
"dev": true
},
+ "perfectionist": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/perfectionist/-/perfectionist-2.4.0.tgz",
+ "integrity": "sha1-wUetNxThJkZ/F2QSnuct+GHUfqA=",
+ "dev": true,
+ "requires": {
+ "comment-regex": "^1.0.0",
+ "defined": "^1.0.0",
+ "minimist": "^1.2.0",
+ "postcss": "^5.0.8",
+ "postcss-scss": "^0.3.0",
+ "postcss-value-parser": "^3.3.0",
+ "read-file-stdin": "^0.2.0",
+ "string.prototype.repeat": "^0.2.0",
+ "vendors": "^1.0.0",
+ "write-file-stdout": "0.0.2"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+ "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+ "dev": true
+ },
+ "postcss": {
+ "version": "5.2.18",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
+ "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
+ "dev": true,
+ "requires": {
+ "chalk": "^1.1.3",
+ "js-base64": "^2.1.9",
+ "source-map": "^0.5.6",
+ "supports-color": "^3.2.3"
+ }
+ },
+ "postcss-scss": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-0.3.1.tgz",
+ "integrity": "sha1-ZcYQ2OKn7g5isYNbcbiHBzSBbks=",
+ "dev": true,
+ "requires": {
+ "postcss": "^5.2.4"
+ }
+ },
+ "supports-color": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+ "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+ "dev": true,
+ "requires": {
+ "has-flag": "^1.0.0"
+ }
+ }
+ }
+ },
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@@ -11830,6 +11922,18 @@
"postcss": "^6.0.1"
}
},
+ "postcss-functions": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz",
+ "integrity": "sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.2",
+ "object-assign": "^4.1.1",
+ "postcss": "^6.0.9",
+ "postcss-value-parser": "^3.3.0"
+ }
+ },
"postcss-html": {
"version": "0.34.0",
"resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-0.34.0.tgz",
@@ -11919,6 +12023,16 @@
"postcss": "^6.0.1"
}
},
+ "postcss-js": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-1.0.1.tgz",
+ "integrity": "sha512-smhUUMF5o5W1ZCQSyh5A3lNOXFLdNrxqyhWbLsGolZH2AgVmlyhxhYbIixfsdKE6r1vG5i7O40DPcvEvE1mvjw==",
+ "dev": true,
+ "requires": {
+ "camelcase-css": "^1.0.1",
+ "postcss": "^6.0.11"
+ }
+ },
"postcss-jsx": {
"version": "0.35.0",
"resolved": "https://registry.npmjs.org/postcss-jsx/-/postcss-jsx-0.35.0.tgz",
@@ -12185,6 +12299,29 @@
"postcss": "^6.0.1"
}
},
+ "postcss-nested": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-3.0.0.tgz",
+ "integrity": "sha512-1xxmLHSfubuUi6xZZ0zLsNoiKfk3BWQj6fkNMaBJC529wKKLcdeCxXt6KJmDLva+trNyQNwEaE/ZWMA7cve1fA==",
+ "dev": true,
+ "requires": {
+ "postcss": "^6.0.14",
+ "postcss-selector-parser": "^3.1.1"
+ },
+ "dependencies": {
+ "postcss-selector-parser": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz",
+ "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=",
+ "dev": true,
+ "requires": {
+ "dot-prop": "^4.1.1",
+ "indexes-of": "^1.0.1",
+ "uniq": "^1.0.1"
+ }
+ }
+ }
+ },
"postcss-nesting": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-4.2.1.tgz",
@@ -12848,6 +12985,15 @@
}
}
},
+ "read-file-stdin": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/read-file-stdin/-/read-file-stdin-0.2.1.tgz",
+ "integrity": "sha1-JezP86FTtoCa+ssj7hU4fbng7mE=",
+ "dev": true,
+ "requires": {
+ "gather-stream": "^1.0.0"
+ }
+ },
"read-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
@@ -14348,6 +14494,12 @@
"function-bind": "^1.0.2"
}
},
+ "string.prototype.repeat": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz",
+ "integrity": "sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8=",
+ "dev": true
+ },
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@@ -15080,6 +15232,48 @@
}
}
},
+ "tailwindcss": {
+ "version": "0.6.6",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-0.6.6.tgz",
+ "integrity": "sha512-g6xb7kcPIom85K7ak16AUBrwN3tPdhrQoKJ7Jl7OJ3zBOQNHthquZ1/q+0V6fj9jsC66jrDCQxn72DIjK4aYgg==",
+ "dev": true,
+ "requires": {
+ "commander": "^2.11.0",
+ "css.escape": "^1.5.1",
+ "fs-extra": "^4.0.2",
+ "lodash": "^4.17.5",
+ "perfectionist": "^2.4.0",
+ "postcss": "^6.0.9",
+ "postcss-functions": "^3.0.0",
+ "postcss-js": "^1.0.1",
+ "postcss-nested": "^3.0.0",
+ "postcss-selector-parser": "^3.1.1"
+ },
+ "dependencies": {
+ "fs-extra": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz",
+ "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
+ "postcss-selector-parser": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz",
+ "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=",
+ "dev": true,
+ "requires": {
+ "dot-prop": "^4.1.1",
+ "indexes-of": "^1.0.1",
+ "uniq": "^1.0.1"
+ }
+ }
+ }
+ },
"tapable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.0.tgz",
@@ -15941,6 +16135,12 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
+ "vendors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.2.tgz",
+ "integrity": "sha512-w/hry/368nO21AN9QljsaIhb9ZiZtZARoVH5f3CsFbawdLdayCgKRPup7CggujvySMxx0I91NOyxdVENohprLQ==",
+ "dev": true
+ },
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
@@ -16895,6 +17095,12 @@
"signal-exit": "^3.0.2"
}
},
+ "write-file-stdout": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/write-file-stdout/-/write-file-stdout-0.0.2.tgz",
+ "integrity": "sha1-wlLXx8WxtAKJdjDjRTx7/mkNnKE=",
+ "dev": true
+ },
"ws": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
diff --git a/package.json b/package.json
index 24855857..21fff587 100644
--- a/package.json
+++ b/package.json
@@ -114,6 +114,7 @@
"stylelint-no-unsupported-browser-features": "^3.0.2",
"svgo": "^1.1.1",
"svgo-loader": "^2.2.0",
+ "tailwindcss": "^0.6.6",
"testpilot-ga": "^0.3.0",
"val-loader": "^1.1.1",
"wdio-docker-service": "^1.4.2",
diff --git a/postcss.config.js b/postcss.config.js
index edb5653d..96d9dc1c 100644
--- a/postcss.config.js
+++ b/postcss.config.js
@@ -1,6 +1,7 @@
const options = {
plugins: {
'postcss-import': {},
+ tailwindcss: {},
'postcss-cssnext': {},
'css-mqpacker': {}
}
diff --git a/public/locales/en-US/send.ftl b/public/locales/en-US/send.ftl
index 2cd40990..7e7da6d2 100644
--- a/public/locales/en-US/send.ftl
+++ b/public/locales/en-US/send.ftl
@@ -40,6 +40,10 @@ timespanMinutes = { $num ->
[one] 1 minute
*[other] { $num } minutes
}
+timespanDays = { $num ->
+ [one] 1 day
+ *[other] { $num } days
+ }
timespanWeeks = { $num ->
[one] 1 week
*[other] { $num } weeks
diff --git a/server/fxa.js b/server/fxa.js
index 9e1b03ed..84a13fd4 100644
--- a/server/fxa.js
+++ b/server/fxa.js
@@ -9,7 +9,10 @@ async function getFxaConfig() {
if (fxaConfig && Date.now() - lastConfigRefresh < 1000 * 60 * 5) {
return fxaConfig;
}
- const res = await fetch(`${config.fxa_url}/.well-known/openid-configuration`);
+ const res = await fetch(
+ `${config.fxa_url}/.well-known/openid-configuration`,
+ { timeout: 3000 }
+ );
fxaConfig = await res.json();
lastConfigRefresh = Date.now();
return fxaConfig;
diff --git a/server/routes/jsconfig.js b/server/routes/jsconfig.js
index 6531e42d..1f511ffc 100644
--- a/server/routes/jsconfig.js
+++ b/server/routes/jsconfig.js
@@ -31,9 +31,13 @@ if (config.analytics_id) {
module.exports = async function(req, res) {
let authConfig = '';
if (config.fxa_client_id) {
- const fxaConfig = await getFxaConfig();
- fxaConfig.client_id = config.fxa_client_id;
- authConfig = `var AUTH_CONFIG = ${JSON.stringify(fxaConfig)};`;
+ try {
+ const fxaConfig = await getFxaConfig();
+ fxaConfig.client_id = config.fxa_client_id;
+ authConfig = `var AUTH_CONFIG = ${JSON.stringify(fxaConfig)};`;
+ } catch (e) {
+ // continue without accounts
+ }
}
/* eslint-disable no-useless-escape */
const jsconfig = `
diff --git a/tailwind.js b/tailwind.js
new file mode 100644
index 00000000..63d53e96
--- /dev/null
+++ b/tailwind.js
@@ -0,0 +1,920 @@
+/*
+
+Tailwind - The Utility-First CSS Framework
+
+A project by Adam Wathan (@adamwathan), Jonathan Reinink (@reinink),
+David Hemphill (@davidhemphill) and Steve Schoger (@steveschoger).
+
+Welcome to the Tailwind config file. This is where you can customize
+Tailwind specifically for your project. Don't be intimidated by the
+length of this file. It's really just a big JavaScript object and
+we've done our very best to explain each section.
+
+View the full documentation at https://tailwindcss.com.
+
+
+|-------------------------------------------------------------------------------
+| The default config
+|-------------------------------------------------------------------------------
+|
+| This variable contains the default Tailwind config. You don't have
+| to use it, but it can sometimes be helpful to have available. For
+| example, you may choose to merge your custom configuration
+| values with some of the Tailwind defaults.
+|
+*/
+
+// let defaultConfig = require('tailwindcss/defaultConfig')()
+
+/*
+|-------------------------------------------------------------------------------
+| Colors https://tailwindcss.com/docs/colors
+|-------------------------------------------------------------------------------
+|
+| Here you can specify the colors used in your project. To get you started,
+| we've provided a generous palette of great looking colors that are perfect
+| for prototyping, but don't hesitate to change them for your project. You
+| own these colors, nothing will break if you change everything about them.
+|
+| We've used literal color names ("red", "blue", etc.) for the default
+| palette, but if you'd rather use functional names like "primary" and
+| "secondary", or even a numeric scale like "100" and "200", go for it.
+|
+*/
+
+const colors = {
+ transparent: 'transparent',
+
+ black: '#22292f',
+ 'grey-darkest': '#3d4852',
+ 'grey-darker': '#606f7b',
+ 'grey-dark': '#8795a1',
+ grey: '#b8c2cc',
+ 'grey-light': '#dae1e7',
+ 'grey-lighter': '#f1f5f8',
+ 'grey-lightest': '#f8fafc',
+ white: '#ffffff',
+
+ 'red-darkest': '#3b0d0c',
+ 'red-darker': '#621b18',
+ 'red-dark': '#cc1f1a',
+ red: '#e3342f',
+ 'red-light': '#ef5753',
+ 'red-lighter': '#f9acaa',
+ 'red-lightest': '#fcebea',
+
+ 'orange-darkest': '#462a16',
+ 'orange-darker': '#613b1f',
+ 'orange-dark': '#de751f',
+ orange: '#f6993f',
+ 'orange-light': '#faad63',
+ 'orange-lighter': '#fcd9b6',
+ 'orange-lightest': '#fff5eb',
+
+ 'yellow-darkest': '#453411',
+ 'yellow-darker': '#684f1d',
+ 'yellow-dark': '#f2d024',
+ yellow: '#ffed4a',
+ 'yellow-light': '#fff382',
+ 'yellow-lighter': '#fff9c2',
+ 'yellow-lightest': '#fcfbeb',
+
+ 'green-darkest': '#0f2f21',
+ 'green-darker': '#1a4731',
+ 'green-dark': '#1f9d55',
+ green: '#38c172',
+ 'green-light': '#51d88a',
+ 'green-lighter': '#a2f5bf',
+ 'green-lightest': '#e3fcec',
+
+ 'teal-darkest': '#0d3331',
+ 'teal-darker': '#20504f',
+ 'teal-dark': '#38a89d',
+ teal: '#4dc0b5',
+ 'teal-light': '#64d5ca',
+ 'teal-lighter': '#a0f0ed',
+ 'teal-lightest': '#e8fffe',
+
+ 'blue-darkest': '#12283a',
+ 'blue-darker': '#1c3d5a',
+ 'blue-dark': '#2779bd',
+ blue: '#3490dc',
+ 'blue-light': '#6cb2eb',
+ 'blue-lighter': '#bcdefa',
+ 'blue-lightest': '#eff8ff',
+
+ 'indigo-darkest': '#191e38',
+ 'indigo-darker': '#2f365f',
+ 'indigo-dark': '#5661b3',
+ indigo: '#6574cd',
+ 'indigo-light': '#7886d7',
+ 'indigo-lighter': '#b2b7ff',
+ 'indigo-lightest': '#e6e8ff',
+
+ 'purple-darkest': '#21183c',
+ 'purple-darker': '#382b5f',
+ 'purple-dark': '#794acf',
+ purple: '#9561e2',
+ 'purple-light': '#a779e9',
+ 'purple-lighter': '#d6bbfc',
+ 'purple-lightest': '#f3ebff',
+
+ 'pink-darkest': '#451225',
+ 'pink-darker': '#6f213f',
+ 'pink-dark': '#eb5286',
+ pink: '#f66d9b',
+ 'pink-light': '#fa7ea8',
+ 'pink-lighter': '#ffbbca',
+ 'pink-lightest': '#ffebef'
+};
+
+module.exports = {
+ /*
+ |-----------------------------------------------------------------------------
+ | Colors https://tailwindcss.com/docs/colors
+ |-----------------------------------------------------------------------------
+ |
+ | The color palette defined above is also assigned to the "colors" key of
+ | your Tailwind config. This makes it easy to access them in your CSS
+ | using Tailwind's config helper. For example:
+ |
+ | .error { color: config('colors.red') }
+ |
+ */
+
+ colors: colors,
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Screens https://tailwindcss.com/docs/responsive-design
+ |-----------------------------------------------------------------------------
+ |
+ | Screens in Tailwind are translated to CSS media queries. They define the
+ | responsive breakpoints for your project. By default Tailwind takes a
+ | "mobile first" approach, where each screen size represents a minimum
+ | viewport width. Feel free to have as few or as many screens as you
+ | want, naming them in whatever way you'd prefer for your project.
+ |
+ | Tailwind also allows for more complex screen definitions, which can be
+ | useful in certain situations. Be sure to see the full responsive
+ | documentation for a complete list of options.
+ |
+ | Class name: .{screen}:{utility}
+ |
+ */
+
+ screens: {
+ sm: '576px',
+ md: '768px',
+ lg: '992px',
+ xl: '1200px'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Fonts https://tailwindcss.com/docs/fonts
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your project's font stack, or font families.
+ | Keep in mind that Tailwind doesn't actually load any fonts for you.
+ | If you're using custom fonts you'll need to import them prior to
+ | defining them here.
+ |
+ | By default we provide a native font stack that works remarkably well on
+ | any device or OS you're using, since it just uses the default fonts
+ | provided by the platform.
+ |
+ | Class name: .font-{name}
+ |
+ */
+
+ fonts: {
+ sans: [
+ 'system-ui',
+ 'BlinkMacSystemFont',
+ '-apple-system',
+ 'Segoe UI',
+ 'Roboto',
+ 'Oxygen',
+ 'Ubuntu',
+ 'Cantarell',
+ 'Fira Sans',
+ 'Droid Sans',
+ 'Helvetica Neue',
+ 'sans-serif'
+ ],
+ serif: [
+ 'Constantia',
+ 'Lucida Bright',
+ 'Lucidabright',
+ 'Lucida Serif',
+ 'Lucida',
+ 'DejaVu Serif',
+ 'Bitstream Vera Serif',
+ 'Liberation Serif',
+ 'Georgia',
+ 'serif'
+ ],
+ mono: [
+ 'Menlo',
+ 'Monaco',
+ 'Consolas',
+ 'Liberation Mono',
+ 'Courier New',
+ 'monospace'
+ ]
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Text sizes https://tailwindcss.com/docs/text-sizing
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your text sizes. Name these in whatever way
+ | makes the most sense to you. We use size names by default, but
+ | you're welcome to use a numeric scale or even something else
+ | entirely.
+ |
+ | By default Tailwind uses the "rem" unit type for most measurements.
+ | This allows you to set a root font size which all other sizes are
+ | then based on. That said, you are free to use whatever units you
+ | prefer, be it rems, ems, pixels or other.
+ |
+ | Class name: .text-{size}
+ |
+ */
+
+ textSizes: {
+ xs: '.75rem', // 12px
+ sm: '.875rem', // 14px
+ base: '1rem', // 16px
+ lg: '1.125rem', // 18px
+ xl: '1.25rem', // 20px
+ '2xl': '1.5rem', // 24px
+ '3xl': '1.875rem', // 30px
+ '4xl': '2.25rem', // 36px
+ '5xl': '3rem' // 48px
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Font weights https://tailwindcss.com/docs/font-weight
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your font weights. We've provided a list of
+ | common font weight names with their respective numeric scale values
+ | to get you started. It's unlikely that your project will require
+ | all of these, so we recommend removing those you don't need.
+ |
+ | Class name: .font-{weight}
+ |
+ */
+
+ fontWeights: {
+ hairline: 100,
+ thin: 200,
+ light: 300,
+ normal: 400,
+ medium: 500,
+ semibold: 600,
+ bold: 700,
+ extrabold: 800,
+ black: 900
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Leading (line height) https://tailwindcss.com/docs/line-height
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your line height values, or as we call
+ | them in Tailwind, leadings.
+ |
+ | Class name: .leading-{size}
+ |
+ */
+
+ leading: {
+ none: 1,
+ tight: 1.25,
+ normal: 1.5,
+ loose: 2
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Tracking (letter spacing) https://tailwindcss.com/docs/letter-spacing
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your letter spacing values, or as we call
+ | them in Tailwind, tracking.
+ |
+ | Class name: .tracking-{size}
+ |
+ */
+
+ tracking: {
+ tight: '-0.05em',
+ normal: '0',
+ wide: '0.05em'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Text colors https://tailwindcss.com/docs/text-color
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your text colors. By default these use the
+ | color palette we defined above, however you're welcome to set these
+ | independently if that makes sense for your project.
+ |
+ | Class name: .text-{color}
+ |
+ */
+
+ textColors: colors,
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Background colors https://tailwindcss.com/docs/background-color
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your background colors. By default these use
+ | the color palette we defined above, however you're welcome to set
+ | these independently if that makes sense for your project.
+ |
+ | Class name: .bg-{color}
+ |
+ */
+
+ backgroundColors: colors,
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Background sizes https://tailwindcss.com/docs/background-size
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your background sizes. We provide some common
+ | values that are useful in most projects, but feel free to add other sizes
+ | that are specific to your project here as well.
+ |
+ | Class name: .bg-{size}
+ |
+ */
+
+ backgroundSize: {
+ auto: 'auto',
+ cover: 'cover',
+ contain: 'contain'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Border widths https://tailwindcss.com/docs/border-width
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your border widths. Take note that border
+ | widths require a special "default" value set as well. This is the
+ | width that will be used when you do not specify a border width.
+ |
+ | Class name: .border{-side?}{-width?}
+ |
+ */
+
+ borderWidths: {
+ default: '1px',
+ '0': '0',
+ '2': '2px',
+ '4': '4px',
+ '8': '8px'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Border colors https://tailwindcss.com/docs/border-color
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your border colors. By default these use the
+ | color palette we defined above, however you're welcome to set these
+ | independently if that makes sense for your project.
+ |
+ | Take note that border colors require a special "default" value set
+ | as well. This is the color that will be used when you do not
+ | specify a border color.
+ |
+ | Class name: .border-{color}
+ |
+ */
+
+ borderColors: global.Object.assign({ default: colors['grey-light'] }, colors),
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Border radius https://tailwindcss.com/docs/border-radius
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your border radius values. If a `default` radius
+ | is provided, it will be made available as the non-suffixed `.rounded`
+ | utility.
+ |
+ | If your scale includes a `0` value to reset already rounded corners, it's
+ | a good idea to put it first so other values are able to override it.
+ |
+ | Class name: .rounded{-side?}{-size?}
+ |
+ */
+
+ borderRadius: {
+ none: '0',
+ sm: '.125rem',
+ default: '.25rem',
+ lg: '.5rem',
+ full: '9999px'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Width https://tailwindcss.com/docs/width
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your width utility sizes. These can be
+ | percentage based, pixels, rems, or any other units. By default
+ | we provide a sensible rem based numeric scale, a percentage
+ | based fraction scale, plus some other common use-cases. You
+ | can, of course, modify these values as needed.
+ |
+ |
+ | It's also worth mentioning that Tailwind automatically escapes
+ | invalid CSS class name characters, which allows you to have
+ | awesome classes like .w-2/3.
+ |
+ | Class name: .w-{size}
+ |
+ */
+
+ width: {
+ auto: 'auto',
+ px: '1px',
+ '1': '0.25rem',
+ '2': '0.5rem',
+ '3': '0.75rem',
+ '4': '1rem',
+ '5': '1.25rem',
+ '6': '1.5rem',
+ '8': '2rem',
+ '10': '2.5rem',
+ '12': '3rem',
+ '16': '4rem',
+ '24': '6rem',
+ '32': '8rem',
+ '48': '12rem',
+ '64': '16rem',
+ '1/2': '50%',
+ '1/3': '33.33333%',
+ '2/3': '66.66667%',
+ '1/4': '25%',
+ '3/4': '75%',
+ '1/5': '20%',
+ '2/5': '40%',
+ '3/5': '60%',
+ '4/5': '80%',
+ '1/6': '16.66667%',
+ '5/6': '83.33333%',
+ full: '100%',
+ screen: '100vw'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Height https://tailwindcss.com/docs/height
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your height utility sizes. These can be
+ | percentage based, pixels, rems, or any other units. By default
+ | we provide a sensible rem based numeric scale plus some other
+ | common use-cases. You can, of course, modify these values as
+ | needed.
+ |
+ | Class name: .h-{size}
+ |
+ */
+
+ height: {
+ auto: 'auto',
+ px: '1px',
+ '1': '0.25rem',
+ '2': '0.5rem',
+ '3': '0.75rem',
+ '4': '1rem',
+ '5': '1.25rem',
+ '6': '1.5rem',
+ '8': '2rem',
+ '10': '2.5rem',
+ '12': '3rem',
+ '16': '4rem',
+ '24': '6rem',
+ '32': '8rem',
+ '48': '12rem',
+ '64': '16rem',
+ full: '100%',
+ screen: '100vh'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Minimum width https://tailwindcss.com/docs/min-width
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your minimum width utility sizes. These can
+ | be percentage based, pixels, rems, or any other units. We provide a
+ | couple common use-cases by default. You can, of course, modify
+ | these values as needed.
+ |
+ | Class name: .min-w-{size}
+ |
+ */
+
+ minWidth: {
+ '0': '0',
+ full: '100%'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Minimum height https://tailwindcss.com/docs/min-height
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your minimum height utility sizes. These can
+ | be percentage based, pixels, rems, or any other units. We provide a
+ | few common use-cases by default. You can, of course, modify these
+ | values as needed.
+ |
+ | Class name: .min-h-{size}
+ |
+ */
+
+ minHeight: {
+ '0': '0',
+ full: '100%',
+ screen: '100vh'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Maximum width https://tailwindcss.com/docs/max-width
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your maximum width utility sizes. These can
+ | be percentage based, pixels, rems, or any other units. By default
+ | we provide a sensible rem based scale and a "full width" size,
+ | which is basically a reset utility. You can, of course,
+ | modify these values as needed.
+ |
+ | Class name: .max-w-{size}
+ |
+ */
+
+ maxWidth: {
+ xs: '20rem',
+ sm: '30rem',
+ md: '40rem',
+ lg: '50rem',
+ xl: '60rem',
+ '2xl': '70rem',
+ '3xl': '80rem',
+ '4xl': '90rem',
+ '5xl': '100rem',
+ full: '100%'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Maximum height https://tailwindcss.com/docs/max-height
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your maximum height utility sizes. These can
+ | be percentage based, pixels, rems, or any other units. We provide a
+ | couple common use-cases by default. You can, of course, modify
+ | these values as needed.
+ |
+ | Class name: .max-h-{size}
+ |
+ */
+
+ maxHeight: {
+ full: '100%',
+ screen: '100vh'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Padding https://tailwindcss.com/docs/padding
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your padding utility sizes. These can be
+ | percentage based, pixels, rems, or any other units. By default we
+ | provide a sensible rem based numeric scale plus a couple other
+ | common use-cases like "1px". You can, of course, modify these
+ | values as needed.
+ |
+ | Class name: .p{side?}-{size}
+ |
+ */
+
+ padding: {
+ px: '1px',
+ '0': '0',
+ '1': '0.25rem',
+ '2': '0.5rem',
+ '3': '0.75rem',
+ '4': '1rem',
+ '5': '1.25rem',
+ '6': '1.5rem',
+ '8': '2rem',
+ '10': '2.5rem',
+ '12': '3rem',
+ '16': '4rem',
+ '20': '5rem',
+ '24': '6rem',
+ '32': '8rem'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Margin https://tailwindcss.com/docs/margin
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your margin utility sizes. These can be
+ | percentage based, pixels, rems, or any other units. By default we
+ | provide a sensible rem based numeric scale plus a couple other
+ | common use-cases like "1px". You can, of course, modify these
+ | values as needed.
+ |
+ | Class name: .m{side?}-{size}
+ |
+ */
+
+ margin: {
+ auto: 'auto',
+ px: '1px',
+ '0': '0',
+ '1': '0.25rem',
+ '2': '0.5rem',
+ '3': '0.75rem',
+ '4': '1rem',
+ '5': '1.25rem',
+ '6': '1.5rem',
+ '8': '2rem',
+ '10': '2.5rem',
+ '12': '3rem',
+ '16': '4rem',
+ '20': '5rem',
+ '24': '6rem',
+ '32': '8rem'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Negative margin https://tailwindcss.com/docs/negative-margin
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your negative margin utility sizes. These can
+ | be percentage based, pixels, rems, or any other units. By default we
+ | provide matching values to the padding scale since these utilities
+ | generally get used together. You can, of course, modify these
+ | values as needed.
+ |
+ | Class name: .-m{side?}-{size}
+ |
+ */
+
+ negativeMargin: {
+ px: '1px',
+ '0': '0',
+ '1': '0.25rem',
+ '2': '0.5rem',
+ '3': '0.75rem',
+ '4': '1rem',
+ '5': '1.25rem',
+ '6': '1.5rem',
+ '8': '2rem',
+ '10': '2.5rem',
+ '12': '3rem',
+ '16': '4rem',
+ '20': '5rem',
+ '24': '6rem',
+ '32': '8rem'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Shadows https://tailwindcss.com/docs/shadows
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your shadow utilities. As you can see from
+ | the defaults we provide, it's possible to apply multiple shadows
+ | per utility using comma separation.
+ |
+ | If a `default` shadow is provided, it will be made available as the non-
+ | suffixed `.shadow` utility.
+ |
+ | Class name: .shadow-{size?}
+ |
+ */
+
+ shadows: {
+ default: '0 2px 4px 0 rgba(0,0,0,0.10)',
+ md: '0 4px 8px 0 rgba(0,0,0,0.12), 0 2px 4px 0 rgba(0,0,0,0.08)',
+ lg: '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)',
+ inner: 'inset 0 2px 4px 0 rgba(0,0,0,0.06)',
+ outline: '0 0 0 3px rgba(52,144,220,0.5)',
+ none: 'none'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Z-index https://tailwindcss.com/docs/z-index
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your z-index utility values. By default we
+ | provide a sensible numeric scale. You can, of course, modify these
+ | values as needed.
+ |
+ | Class name: .z-{index}
+ |
+ */
+
+ zIndex: {
+ auto: 'auto',
+ '0': 0,
+ '10': 10,
+ '20': 20,
+ '30': 30,
+ '40': 40,
+ '50': 50
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Opacity https://tailwindcss.com/docs/opacity
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your opacity utility values. By default we
+ | provide a sensible numeric scale. You can, of course, modify these
+ | values as needed.
+ |
+ | Class name: .opacity-{name}
+ |
+ */
+
+ opacity: {
+ '0': '0',
+ '25': '.25',
+ '50': '.5',
+ '75': '.75',
+ '100': '1'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | SVG fill https://tailwindcss.com/docs/svg
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your SVG fill colors. By default we just provide
+ | `fill-current` which sets the fill to the current text color. This lets you
+ | specify a fill color using existing text color utilities and helps keep the
+ | generated CSS file size down.
+ |
+ | Class name: .fill-{name}
+ |
+ */
+
+ svgFill: {
+ current: 'currentColor'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | SVG stroke https://tailwindcss.com/docs/svg
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you define your SVG stroke colors. By default we just provide
+ | `stroke-current` which sets the stroke to the current text color. This lets
+ | you specify a stroke color using existing text color utilities and helps
+ | keep the generated CSS file size down.
+ |
+ | Class name: .stroke-{name}
+ |
+ */
+
+ svgStroke: {
+ current: 'currentColor'
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Modules https://tailwindcss.com/docs/configuration#modules
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you control which modules are generated and what variants are
+ | generated for each of those modules.
+ |
+ | Currently supported variants:
+ | - responsive
+ | - hover
+ | - focus
+ | - active
+ | - group-hover
+ |
+ | To disable a module completely, use `false` instead of an array.
+ |
+ */
+
+ modules: {
+ appearance: ['responsive'],
+ backgroundAttachment: ['responsive'],
+ backgroundColors: ['responsive', 'hover', 'focus'],
+ backgroundPosition: ['responsive'],
+ backgroundRepeat: ['responsive'],
+ backgroundSize: ['responsive'],
+ borderCollapse: [],
+ borderColors: ['responsive', 'hover', 'focus'],
+ borderRadius: ['responsive'],
+ borderStyle: ['responsive'],
+ borderWidths: ['responsive'],
+ cursor: ['responsive'],
+ display: ['responsive'],
+ flexbox: ['responsive'],
+ float: ['responsive'],
+ fonts: ['responsive'],
+ fontWeights: ['responsive', 'hover', 'focus'],
+ height: ['responsive'],
+ leading: ['responsive'],
+ lists: ['responsive'],
+ margin: ['responsive'],
+ maxHeight: ['responsive'],
+ maxWidth: ['responsive'],
+ minHeight: ['responsive'],
+ minWidth: ['responsive'],
+ negativeMargin: ['responsive'],
+ opacity: ['responsive'],
+ outline: ['focus'],
+ overflow: ['responsive'],
+ padding: ['responsive'],
+ pointerEvents: ['responsive'],
+ position: ['responsive'],
+ resize: ['responsive'],
+ shadows: ['responsive', 'hover', 'focus'],
+ svgFill: [],
+ svgStroke: [],
+ tableLayout: ['responsive'],
+ textAlign: ['responsive'],
+ textColors: ['responsive', 'hover', 'focus'],
+ textSizes: ['responsive'],
+ textStyle: ['responsive', 'hover', 'focus'],
+ tracking: ['responsive'],
+ userSelect: ['responsive'],
+ verticalAlign: ['responsive'],
+ visibility: ['responsive'],
+ whitespace: ['responsive'],
+ width: ['responsive'],
+ zIndex: ['responsive']
+ },
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Plugins https://tailwindcss.com/docs/plugins
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you can register any plugins you'd like to use in your
+ | project. Tailwind's built-in `container` plugin is enabled by default to
+ | give you a Bootstrap-style responsive container component out of the box.
+ |
+ | Be sure to view the complete plugin documentation to learn more about how
+ | the plugin system works.
+ |
+ */
+
+ plugins: [
+ require('tailwindcss/plugins/container')({
+ // center: true,
+ // padding: '1rem',
+ })
+ ],
+
+ /*
+ |-----------------------------------------------------------------------------
+ | Advanced Options https://tailwindcss.com/docs/configuration#options
+ |-----------------------------------------------------------------------------
+ |
+ | Here is where you can tweak advanced configuration options. We recommend
+ | leaving these options alone unless you absolutely need to change them.
+ |
+ */
+
+ options: {
+ prefix: '',
+ important: false,
+ separator: ':'
+ }
+};