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)}
    + +

    + ${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}
    + ${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}
    + ${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` +
    +
    + ${content} +
    +
    `; +}; 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` +
    +
    + Firefox + Send is brought to you by the all-new Firefox. + Download Firefox now ≫ + +
    +
    `; +}; 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')} +

    +

    +
    + + +
    + + +
    `; + + 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` +
    +
    +

    ${strings.header}

    +

    + ${strings.description} +

    + ${why} + + Firefox + ${buttonAction} + +
    +
    `; +}; + +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: ':' + } +};