0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-03 23:00:14 -05:00

Added members lib module (#10260)

* Added members library inc. gateway

refs #10213

* Added the auth pages and build steps for them

refs #10213

* Cleaned up logs

* Updated gruntfile to run yarn for member auth

* Design refinements on members popups

* UI refinements

* Updated backend call to trigger only if frontend validation passes

* Design refinements for error messages

* Added error message for email failure

* Updated request-password-reset to not attempt to send headers twice

* Updated preact publicPath to relative path

* Build auth pages on init
This commit is contained in:
Fabien O'Carroll 2018-12-11 13:47:44 +07:00
parent e511fcf4d9
commit b219e26ea6
23 changed files with 11469 additions and 0 deletions

View file

@ -0,0 +1,51 @@
const crypto = require('crypto');
const cookie = require('cookie');
const MAX_AGE = 60 * 60 * 24 * 184;
module.exports = function cookies(sessionSecret) {
function encodeCookie(data) {
const encodedData = encodeURIComponent(data);
const hmac = crypto.createHmac('sha256', sessionSecret);
hmac.update(encodedData);
return `${hmac.digest('hex')}~${encodedData}`;
}
function decodeCookie(data) {
const hmac = crypto.createHmac('sha256', sessionSecret);
const [sentHmac, sentData] = data.split('~');
if (hmac.update(sentData).digest('hex') !== sentHmac) {
return null;
}
return decodeURIComponent(sentData);
}
function setCookie(member) {
return cookie.serialize('signedin', member.id, {
maxAge: MAX_AGE,
path: '/ghost/api/v2/members/token',
httpOnly: true,
encode: encodeCookie
});
}
function removeCookie() {
return cookie.serialize('signedin', false, {
maxAge: 0,
path: '/ghost/api/v2/members/token',
httpOnly: true
});
}
function getCookie(req) {
return cookie.parse(req.headers.cookie || '', {
decode: decodeCookie
});
}
return {
setCookie,
removeCookie,
getCookie
};
};

206
ghost/members-api/index.js Normal file
View file

@ -0,0 +1,206 @@
const jose = require('node-jose');
const {Router, static} = require('express');
const body = require('body-parser');
const jwt = require('jsonwebtoken');
const cookies = require('./cookies');
module.exports = function MembersApi({
config: {
issuer,
privateKey,
publicKey,
sessionSecret,
ssoOrigin
},
validateAudience,
createMember,
validateMember,
updateMember,
getMember,
sendEmail
}) {
const keyStore = jose.JWK.createKeyStore();
const keyStoreReady = keyStore.add(privateKey, 'pem');
const router = Router();
const apiRouter = Router();
apiRouter.use(body.json());
apiRouter.use(function waitForKeyStore(req, res, next) {
keyStoreReady.then((jwk) => {
req.jwk = jwk;
next();
});
});
const {getCookie, setCookie, removeCookie} = cookies(sessionSecret);
apiRouter.post('/token', getData('audience'), (req, res) => {
const {signedin} = getCookie(req);
if (!signedin) {
res.writeHead(401, {
'Set-Cookie': removeCookie()
});
return res.end();
}
const {audience, origin} = req.data;
validateAudience({audience, origin, id: signedin}).then(() => {
const token = jwt.sign({
sub: signedin,
kid: req.jwk.kid
}, privateKey, {
algorithm: 'RS512',
audience,
issuer
});
return res.end(token);
}).catch(handleError(403, res));
});
function ssoOriginCheck(req, res, next) {
if (!req.data.origin || req.data.origin !== ssoOrigin) {
res.writeHead(403);
return res.end();
}
next();
}
apiRouter.post('/request-password-reset', getData('email'), ssoOriginCheck, (req, res) => {
const {email} = req.data;
const memberPromise = getMember({email});
memberPromise.catch(() => {
res.writeHead(200);
res.end();
});
memberPromise.then((member) => {
const token = jwt.sign({
sub: member.id,
kid: req.jwk.kid
}, privateKey, {
algorithm: 'RS512',
issuer
});
return sendEmail(member, {token});
}).then(() => {
res.writeHead(200);
res.end();
}).catch(handleError(500, res));
});
apiRouter.post('/reset-password', getData('token', 'password'), ssoOriginCheck, (req, res) => {
const {token, password} = req.data;
try {
jwt.verify(token, publicKey, {
algorithm: 'RS512',
issuer
});
} catch (err) {
res.writeHead(401);
return res.end();
}
const id = jwt.decode(token).sub;
updateMember({id}, {password}).then((member) => {
res.writeHead(200, {
'Set-Cookie': setCookie(member)
});
res.end();
}).catch(handleError(401, res));
});
apiRouter.post('/signup', getData('name', 'email', 'password'), ssoOriginCheck, (req, res) => {
const {name, email, password} = req.data;
// @TODO this should attempt to reset password before creating member
createMember({name, email, password}).then((member) => {
res.writeHead(200, {
'Set-Cookie': setCookie(member)
});
res.end();
}).catch(handleError(400, res));
});
apiRouter.post('/signin', getData('email', 'password'), ssoOriginCheck, (req, res) => {
const {email, password} = req.data;
validateMember({email, password}).then((member) => {
res.writeHead(200, {
'Set-Cookie': setCookie(member)
});
res.end();
}).catch(handleError(401, res));
});
apiRouter.post('/signout', getData(), ssoOriginCheck, (req, res) => {
res.writeHead(200, {
'Set-Cookie': removeCookie()
});
res.end();
});
const staticRouter = Router();
staticRouter.use('/static', static(require('path').join(__dirname, './static/auth/dist')));
staticRouter.use('/gateway', static(require('path').join(__dirname, './static/gateway')));
staticRouter.get('/*', (req, res) => {
res.sendFile(require('path').join(__dirname, './static/auth/dist/index.html'));
});
router.use('/api', apiRouter);
router.use('/static', staticRouter);
router.get('/.well-known/jwks.json', (req, res) => {
keyStoreReady.then(() => {
res.json(keyStore.toJSON());
});
});
function httpHandler(req, res, next) {
return router.handle(req, res, next);
}
httpHandler.staticRouter = staticRouter;
httpHandler.apiRouter = apiRouter;
httpHandler.keyStore = keyStore;
return httpHandler;
};
function getData(...props) {
return function (req, res, next) {
if (!req.body) {
res.writeHead(400);
return res.end();
}
const data = props.concat('origin').reduce((data, prop) => {
if (!data || !req.body[prop]) {
return null;
}
return Object.assign(data, {
[prop]: req.body[prop]
});
}, {});
if (!data) {
res.writeHead(400);
return res.end(`Expected {${props.join(', ')}}`);
}
req.data = data || {};
next();
};
}
function handleError(status, res) {
return function () {
res.writeHead(status);
res.end();
};
}

View file

@ -0,0 +1,4 @@
node_modules
/dist
/build
/*.log

View file

@ -0,0 +1,47 @@
/* eslint-env node */
/* eslint-disable object-shorthand */
'use strict';
module.exports = function (grunt) {
// Find all of the task which start with `grunt-` and load them, rather than explicitly declaring them all
require('matchdep').filterDev(['grunt-*', '!grunt-cli']).forEach(grunt.loadNpmTasks);
grunt.initConfig({
clean: {
built: {
src: ['dist/**']
},
dependencies: {
src: ['node_modules/**']
},
tmp: {
src: ['tmp/**']
}
},
shell: {
'npm-install': {
command: 'yarn install'
},
preact: {
command: function (mode) {
switch (mode) {
case 'prod':
return 'yarn build';
case 'dev':
return 'yarn dev';
}
}
},
options: {
preferLocal: true
}
}
});
grunt.registerTask('init', 'Install the preact member dependencies',
['shell:npm-install', 'shell:preact:prod']
);
};

View file

@ -0,0 +1 @@
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Brand/Ghost Logotype - Light</title><g transform="translate(0 .285)" fill-rule="nonzero" fill="#FFF" opacity=".6"><rect x=".049" y="15.43" width="7.901" height="3.804" rx="1.902"/><rect x="11.9" y="15.43" width="7.896" height="3.804" rx="1.902"/><rect x=".043" y="7.822" width="19.755" height="3.804" rx="1.902"/><path d="M1.95.216H10c1.05 0 1.901.85 1.901 1.901 0 1.05-.851 1.902-1.901 1.902H1.95c-1.05 0-1.9-.851-1.9-1.902C.05 1.067.9.216 1.95.216zM17.752.216h.147c1.05 0 1.902.85 1.902 1.901 0 1.05-.852 1.902-1.902 1.902h-.147c-1.05 0-1.901-.851-1.901-1.902 0-1.05.851-1.901 1.901-1.901z"/></g></svg>

After

Width:  |  Height:  |  Size: 694 B

View file

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><title>icon-email</title><g fill="none" fill-rule="evenodd"><path fill-opacity="0" fill="#FFF" fill-rule="nonzero" d="M0 0h16v16H0z"/><path d="M15.619 12.53c0 .646-.38.97-1.143.97H1.524c-.762 0-1.143-.324-1.143-.97V3.47c0-.267.112-.496.335-.686.223-.19.492-.284.808-.284h12.952c.762 0 1.143.324 1.143.97v9.06z" stroke="#B2C2C9" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#B2C2C9" stroke-linecap="round" stroke-linejoin="round" d="M15.238 3L8 10 .762 3"/></g></svg>

After

Width:  |  Height:  |  Size: 566 B

View file

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><title>icon-lock</title><g fill="none" fill-rule="evenodd"><path d="M8.75 10.25c0 .5-.25.75-.75.75s-.75-.25-.75-.75.25-.75.75-.75.75.25.75.75zM8 11v2.25" stroke="#B2C2C9" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.706 6.5h10.588c.47 0 .706.214.706.643v7.714c0 .429-.235.643-.706.643H2.706c-.47 0-.706-.214-.706-.643V7.143c0-.429.235-.643.706-.643zM3.875 4.817c0-1.645.687-2.878 2.063-3.7 1.375-.823 2.75-.823 4.125 0 1.374.822 2.062 2.055 2.062 3.7V6.5h-8.25V4.817z" stroke="#B2C2C9" stroke-linecap="round" stroke-linejoin="round"/></g></svg>

After

Width:  |  Height:  |  Size: 642 B

View file

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><title>icon-name</title><g fill="none" fill-rule="evenodd"><path stroke-opacity=".012" stroke="#000" stroke-width="0" d="M.5.5h15v15H.5z"/><g stroke="#B2C2C9" stroke-linecap="round" stroke-linejoin="round"><path d="M15.187 8c0 1.985-.701 3.679-2.105 5.082C11.68 14.486 9.985 15.187 8 15.187c-1.985 0-3.679-.701-5.082-2.105C1.514 11.68.812 9.985.812 8c0-1.985.702-3.679 2.106-5.082C4.32 1.514 6.015.812 8 .812c1.985 0 3.679.702 5.082 2.106C14.486 4.32 15.187 6.015 15.187 8z"/><path d="M2.974 13.138c1.071-.62 2.199-1.11 3.383-1.47.524-.193.58-1.393.205-1.805-.54-.596-1-1.294-1-2.98-.066-.711.145-1.328.633-1.85.489-.522 1.09-.773 1.805-.754.715-.02 1.316.232 1.805.754.488.522.7 1.139.632 1.85 0 1.688-.458 2.384-1 2.98-.375.412-.318 1.612.205 1.805 1.185.36 2.313.85 3.384 1.47"/></g></g></svg>

After

Width:  |  Height:  |  Size: 879 B

View file

@ -0,0 +1,20 @@
export const IconEmail = (
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><title>icon-email</title><g fill="none" fill-rule="evenodd"><path d="M15.619 12.53c0 .646-.38.97-1.143.97H1.524c-.762 0-1.143-.324-1.143-.97V3.47c0-.267.112-.496.335-.686.223-.19.492-.284.808-.284h12.952c.762 0 1.143.324 1.143.97v9.06z" stroke="#B2C2C9" stroke-linecap="round" stroke-linejoin="round" /><path stroke="#B2C2C9" stroke-linecap="round" stroke-linejoin="round" d="M15.238 3L8 10 .762 3" /></g></svg>
);
export const IconLock = (
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><title>icon-lock</title><g fill="none" fill-rule="evenodd"><path d="M8.75 10.25c0 .5-.25.75-.75.75s-.75-.25-.75-.75.25-.75.75-.75.75.25.75.75zM8 11v2.25" stroke="#B2C2C9" stroke-linecap="round" stroke-linejoin="round" /><path d="M2.706 6.5h10.588c.47 0 .706.214.706.643v7.714c0 .429-.235.643-.706.643H2.706c-.47 0-.706-.214-.706-.643V7.143c0-.429.235-.643.706-.643zM3.875 4.817c0-1.645.687-2.878 2.063-3.7 1.375-.823 2.75-.823 4.125 0 1.374.822 2.062 2.055 2.062 3.7V6.5h-8.25V4.817z" stroke="#B2C2C9" stroke-linecap="round" stroke-linejoin="round" /></g></svg>
);
export const IconName = (
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><title>icon-name</title><g fill="none" fill-rule="evenodd"><path stroke-opacity=".012" stroke="#000" stroke-width="0" d="M.5.5h15v15H.5z" /><g stroke="#B2C2C9" stroke-linecap="round" stroke-linejoin="round"><path d="M15.187 8c0 1.985-.701 3.679-2.105 5.082C11.68 14.486 9.985 15.187 8 15.187c-1.985 0-3.679-.701-5.082-2.105C1.514 11.68.812 9.985.812 8c0-1.985.702-3.679 2.106-5.082C4.32 1.514 6.015.812 8 .812c1.985 0 3.679.702 5.082 2.106C14.486 4.32 15.187 6.015 15.187 8z" /><path d="M2.974 13.138c1.071-.62 2.199-1.11 3.383-1.47.524-.193.58-1.393.205-1.805-.54-.596-1-1.294-1-2.98-.066-.711.145-1.328.633-1.85.489-.522 1.09-.773 1.805-.754.715-.02 1.316.232 1.805.754.488.522.7 1.139.632 1.85 0 1.688-.458 2.384-1 2.98-.375.412-.318 1.612.205 1.805 1.185.36 2.313.85 3.384 1.47" /></g></g></svg>
);
export const IconClose = (
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><title>icon-close</title><g fill="none" fill-rule="evenodd"><path d="M2.25 2.25l11.5 11.5M13.75 2.25l-11.5 11.5" stroke="#9BAEB8" stroke-linecap="round" stroke-linejoin="round" /></g></svg>
);
export const IconError = (
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><title>icon-error</title><g fill="none" fill-rule="evenodd"><path d="M15 7.88c.005 1.944-.674 3.61-2.038 4.997C11.6 14.263 9.945 14.97 8 14.999c-1.921.029-3.567-.63-4.937-1.976S1.005 10.043 1 8.123c-.005-1.946.674-3.612 2.037-4.999C4.401 1.737 6.055 1.029 8 1.001c1.921-.029 3.567.63 4.938 1.977 1.37 1.347 2.057 2.98 2.062 4.902zM7.933 9.337V4.67" stroke="#F05230" stroke-linecap="round" stroke-linejoin="round" /><path d="M7.927 11.67c-.046 0-.084.018-.116.051-.031.033-.046.073-.044.119.004.109.06.163.168.163.046 0 .085-.018.116-.051.032-.033.047-.073.045-.119-.003-.105-.057-.16-.163-.163H7.93" stroke="#F05230" stroke-linecap="round" stroke-linejoin="round" /></g></svg>
);

View file

@ -0,0 +1,395 @@
import './styles/members.css';
import {IconEmail, IconLock, IconName, IconClose, IconError} from './components/icons';
import { Component } from 'preact';
const origin = new URL(window.location).origin;
const membersApi = location.pathname.replace(/\/members\/auth\/?$/, '/ghost/api/v2/members');
const storage = window.localStorage;
var layer0 = require('./layer0');
function getFreshState() {
const [hash, formType, query] = window.location.hash.match(/^#([^?]+)\??(.*)$/) || ['#signin?', 'signin', ''];
return {
formData: {},
query,
formType,
showError: false,
submitFail: false
};
}
export default class App extends Component {
constructor() {
super();
this.state = getFreshState();
this.gatewayFrame = '';
window.addEventListener("hashchange", () => this.onHashChange(), false);
}
loadGateway() {
const blogUrl = window.location.href.substring(0, window.location.href.indexOf('/members/auth'));
const frame = window.document.createElement('iframe');
frame.id = 'member-gateway';
frame.style.display = 'none';
frame.src = `${blogUrl}/members/gateway`;
frame.onload = () => {
this.gatewayFrame = layer0(frame);
};
document.body.appendChild(frame);
}
componentDidMount() {
this.loadGateway();
}
onHashChange() {
this.setState(getFreshState());
}
onInputChange(e, name) {
let value = e.target.value;
this.setState({
formData: {
...this.state.formData,
[name]: value
}
});
}
submitForm(e) {
e.preventDefault();
if (this.hasFrontendError(this.state.formType)) {
return false;
}
switch (this.state.formType) {
case 'signin':
this.signin(this.state.formData);
break;
case 'signup':
this.signup(this.state.formData);
break;
case 'request-password-reset':
this.requestPasswordReset(this.state.formData);
break;
case 'password-reset-sent':
this.resendPasswordResetEmail(this.state.formData)
break;
case 'reset-password':
this.resetPassword(this.state.formData)
break;
}
return false;
}
signin({ email, password }) {
this.gatewayFrame.call('signin', {email, password}, (err, successful) => {
if (err || !successful) {
this.setState({
submitFail: true
});
}
});
}
signup({ name, email, password }) {
this.gatewayFrame.call('signup', { name, email, password }, (err, successful) => {
if (err || !successful) {
this.setState({
submitFail: true
});
}
});
}
requestPasswordReset({ email }) {
this.gatewayFrame.call('request-password-reset', {email}, (err, successful) => {
if (err || !successful) {
this.setState({
submitFail: true
});
} else {
window.location.hash = 'password-reset-sent';
}
});
}
resendPasswordResetEmail({ email }) {
this.gatewayFrame.call('request-password-reset', {email}, (err, successful) => {
if (err || !successful) {
this.setState({
submitFail: true
});
} else {
window.location.hash = 'password-reset-sent';
}
});
}
resetPassword({ password }) {
const queryParams = new URLSearchParams(this.state.query);
const token = queryParams.get('token') || '';
this.gatewayFrame.call('reset-password', {password, token}, (err, successful) => {
if (err || !successful) {
this.setState({
submitFail: true
});
}
});
}
hasFrontendError(formType = this.state.formType) {
switch(formType) {
case 'signin':
return (
this.hasError({errorType: 'no-input', data: 'email'}) ||
this.hasError({errorType: 'no-input', data: 'password'})
);
case 'signup':
return (
this.hasError({errorType: 'no-input', data: 'email'}) ||
this.hasError({errorType: 'no-input', data: 'password'}) ||
this.hasError({errorType: 'no-input', data: 'name'})
);
}
return false;
}
hasError({errorType, data}) {
if (!this.state.showError) {
return false;
}
let value = '';
switch(errorType) {
case 'no-input':
value = this.state.formData[data];
return (!value);
case 'form-submit':
return this.state.submitFail;
}
}
renderError({error, formType}) {
if (this.hasError(error)) {
let errorLabel = '';
switch(error.errorType) {
case 'no-input':
errorLabel = `Enter ${error.data}`;
break;
case 'form-submit':
switch(formType) {
case 'signin':
errorLabel = "Wrong email or password";
break;
case 'signup':
errorLabel = "Email already registered"
break;
case 'request-password-reset':
errorLabel = "Unable to send email"
break;
case 'password-reset-sent':
errorLabel = "Unable to send email"
break;
}
}
return (
<span>{ errorLabel }</span>
)
}
return null;
}
renderFormHeaders(formType) {
let mainTitle = '';
let ctaTitle = '';
let ctaLabel = '';
let hash = '';
switch (formType) {
case 'signup':
mainTitle = 'Sign Up';
ctaTitle = 'Already a member?';
ctaLabel = 'Log in';
hash = 'signin';
break;
case 'signin':
mainTitle = 'Log In';
ctaTitle = 'Not a member?';
ctaLabel = 'Sign up';
hash = 'signup';
break;
case 'request-password-reset':
mainTitle = 'Reset password';
ctaTitle = '';
ctaLabel = 'Log in';
hash = 'signin';
break;
case 'password-reset-sent':
mainTitle = 'Reset password';
ctaTitle = '';
ctaLabel = 'Log in';
hash = 'signin';
break;
case 'reset-password':
mainTitle = 'Reset password';
ctaTitle = '';
ctaLabel = 'Log in';
hash = 'signin';
break;
}
let formError = this.renderError({ error: {errorType: "form-submit"}, formType });
return (
<div className="flex flex-column">
<div className="gm-logo"></div>
<div className="gm-auth-header">
<h1>{ mainTitle }</h1>
<div className="flex items-baseline">
<h4>{ ctaTitle }</h4>
<a href="javascript:;"
onClick={(e) => {window.location.hash = hash}}
>
{ctaLabel}
</a>
</div>
</div>
{(formError ? <div class="gm-form-errortext"><i>{ IconError }</i> { formError }</div> : "")}
</div>
)
}
renderFormInput({type, name, label, icon, placeholder, required, formType}) {
let value = this.state.formData[name];
let className = "";
let forgot = (type === 'password' && formType === 'signin');
let inputError = this.renderError({ error: {errorType: 'no-input', data: name}, formType });
className += (value ? "gm-input-filled" : "") + (forgot ? " gm-forgot-input" : "") + (inputError ? " gm-error" : "");
return (
<div className="mt8">
<div className="gm-floating-input">
<input
type={ type }
name={ name }
key={ name }
placeholder={ placeholder }
value={ value || '' }
onInput={ (e) => this.onInputChange(e, name) }
required = {required}
className={ className }
/>
<label for={ name }> { label }</label>
<i>{ icon }</i>
{ (forgot ? <a href="javascript:;" className="gm-forgot-link" onClick={(e) => {window.location.hash = 'request-password-reset'}}>Forgot</a> : "") }
</div>
<div class="gm-input-errortext">{ inputError }</div>
</div>
)
}
renderFormText({formType}) {
return (
<div className="mt8">
<p>Weve sent a recovery email to your inbox. Follow the link in the email to reset your password.</p>
</div>
)
}
onSubmitClick(e) {
this.setState({
showError: true,
submitFail: false
});
}
renderFormSubmit({buttonLabel, formType}) {
return (
<div className="mt8">
<button type="submit" name={ formType } className="gm-btn-blue" onClick={(e) => this.onSubmitClick(e)}>{ buttonLabel }</button>
</div>
)
}
renderFormSection(formType) {
const emailInput = this.renderFormInput({
type: 'email',
name: 'email',
label: 'Email',
icon: IconEmail,
placeholder: 'Email...',
required: true,
formType: formType
});
const passwordInput = this.renderFormInput({
type: 'password',
name: 'password',
label: 'Password',
icon: IconLock,
placeholder: 'Password...',
required: true,
formType: formType
});
const nameInput = this.renderFormInput({
type: 'text',
name: 'name',
label: 'Name',
icon: IconName,
placeholder: 'Name...',
required: true,
formType: formType
});
const formText = this.renderFormText({formType});
let formElements = [];
let buttonLabel = '';
switch (formType) {
case 'signin':
buttonLabel = 'Log in';
formElements = [emailInput, passwordInput, this.renderFormSubmit({formType, buttonLabel})];
break;
case 'signup':
buttonLabel = 'Sign up';
formElements = [nameInput, emailInput, passwordInput, this.renderFormSubmit({formType, buttonLabel})];
break;
case 'request-password-reset':
buttonLabel = 'Send reset password instructions';
formElements = [emailInput, this.renderFormSubmit({formType, buttonLabel})];
break;
case 'password-reset-sent':
buttonLabel = 'Resend instructions';
formElements = [formText, this.renderFormSubmit({formType, buttonLabel})];
break;
case 'reset-password':
buttonLabel = 'Set password';
formElements = [passwordInput, this.renderFormSubmit({formType, buttonLabel})];
break;
}
return (
<div className="flex flex-column nt1">
<form className={ `gm-` + formType + `-form` } onSubmit={(e) => this.submitForm(e)} noValidate>
{ formElements }
</form>
</div>
)
}
renderFormComponent(formType = this.state.formType) {
return (
<div className="gm-modal-container">
<div className="gm-modal gm-auth-modal" onClick={(e) => e.stopPropagation()}>
<a className="gm-modal-close" onClick={ (e) => this.close(e)}>{ IconClose }</a>
{this.renderFormHeaders(formType)}
{this.renderFormSection(formType)}
</div>
</div>
);
}
render() {
return (
<div className="gm-page-overlay" onClick={(e) => this.close(e)}>
{this.renderFormComponent()}
</div>
);
}
close(event) {
window.parent.postMessage('pls-close-auth-popup', '*');
}
}

View file

@ -0,0 +1,42 @@
/* globals window */
module.exports = function layer0(frame) {
var getuid = (function (i) {
return function () {
return i += 1;
};
})(1);
var origin = new URL(frame.getAttribute('src')).origin;
var handlers = {};
var listener = function () {};
window.addEventListener('message', function (event) {
if (event.origin !== origin) {
return;
}
if (!event.data || !event.data.uid) {
if (event.data.event) {
return listener(event.data);
}
return;
}
var handler = handlers[event.data.uid];
if (!handler) {
return;
}
delete handlers[event.data.uid];
handler(event.data.error, event.data.data);
});
function call(method, options, cb) {
var uid = getuid();
var data = {uid, method, options};
handlers[uid] = cb;
frame.contentWindow.postMessage(data, origin);
}
function listen(fn) {
listener = fn;
}
return {call, listen};
};

View file

@ -0,0 +1,28 @@
{
"name": "ghost-member",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"build": "preact build --src=index.js --dest=dist --service-worker=false --no-prerender",
"dev": "yarn build --no-production && preact watch --port=8080",
"lint": "eslint src"
},
"eslintIgnore": [
"build/*"
],
"devDependencies": {
"autoprefixer": "^9.4.2",
"cssnano": "^4.1.7",
"grunt": "1.0.3",
"grunt-shell": "2.1.0",
"postcss-color-mod-function": "^3.0.3",
"postcss-css-variables": "^0.11.0",
"postcss-custom-properties": "^8.0.9",
"postcss-import": "^12.0.1",
"preact-cli": "^2.0.0"
},
"dependencies": {
"preact": "^8.2.1",
"preact-compat": "^3.17.0"
}
}

View file

@ -0,0 +1,10 @@
module.exports = {
plugins: [
require('postcss-import'),
require('autoprefixer'),
require('postcss-css-variables'),
require('postcss-color-mod-function'),
require('cssnano'),
require('postcss-custom-properties')
]
};

View file

@ -0,0 +1,26 @@
export default function (config, env, helpers) {
const postcssLoader = helpers.getLoadersByName(config, 'postcss-loader');
const cssLoader = helpers.getLoadersByName(config, 'css-loader');
postcssLoader.forEach(({ loader }) => (delete loader.options));
cssLoader.forEach(({ loader }) => (delete loader.options));
helpers.getRulesByMatchingFile(config, '*.css').forEach(({ rule }) => {
let filter = (rule.include || rule.exclude || []);
let newFilter = filter[0].replace('/components', '/styles');
filter.push(newFilter);
});
if (env.production) {
config.output.publicPath = 'static/';
} else {
config.output.publicPath = 'http://localhost:8080/';
}
config.devServer = {
quiet: true,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization"
}
}
}

View file

@ -0,0 +1,292 @@
/* Reusable components */
/* ------------------------------------------------------------
*/
/* Modal */
/* ------------------------------------------------------------ */
.gm-page-overlay {
width: 100%;
height: 100vh;
position: fixed;
overflow-y: scroll;
background: rgba(10, 17, 23, 0.9);
animation: fadeInOverlay 0.2s ease;
}
.gm-modal-container {
position: relative;
top: 50%;
transform: translateY(-50%);
}
.gm-modal {
position: relative;
background: white;
margin: 0 auto;
width: 288px;
border-radius: 4px;
padding: 40px;
box-shadow: var(--box-shadow-base);
animation: openModal 0.6s ease;
}
.gm-modal-close {
position: absolute;
top: 8px;
right: 8px;
display: block;
padding: 8px;
}
.gm-modal-close svg path {
stroke: var(--grey);
transition: all var(--animation-speed-base) ease;
}
.gm-modal-close:hover svg path {
stroke: var(--grey-d2);
}
@keyframes fadeInOverlay {
from {opacity: 0;}
to {opacity: 1;}
}
@keyframes openModal { /* Safari and Chrome */
0% {
opacity: 0;
transform: translateY(25px) scale(0.85);
}
40% {
opacity: 1.0;
transform: translateY(-8px) scale(1.04);
}
100% {
transform: translateY(0) scale(1.0);
}
}
@media (max-width: 440px) {
.gm-modal-container {
margin: 0;
padding: 0;
top: 0;
transform: none;
height: 100vh;
}
.gm-modal {
width: calc(100% - 48px);
height: calc(100vh - 48px);
padding: 24px;
border-radius: 0;
}
}
/* Buttons */
/* ------------------------------------------------------------ */
button {
width: 100%;
height: 44px;
font-weight: 500;
border: 1px solid var(--grey);
color: var(--grey-d3);
text-align: center;
cursor: pointer;
white-space: nowrap;
padding: 0 15px;
border-radius: 4px;
outline: none;
transition: all var(--animation-speed-f1) ease-in-out;
position: relative;
letter-spacing: 0.2px;
}
button:hover {
color: var(--blue-l1);
}
.gm-btn-blue {
background: var(--blue);
background: linear-gradient(to bottom, rgba(62,176,239,1) 0%,rgba(0,139,214,1) 100%);
color: var(--white);
border: none;
}
.gm-btn-blue:active {
background: var(--blue-d1);
background: linear-gradient(to bottom, rgb(22, 147, 214) 0%,rgb(0, 118, 182) 100%);
}
.gm-btn-blue:hover {
color: var(--white);
}
.gm-btn-blue:hover:before {
opacity: 0.8;
}
.gm-btn-blue:before {
content: "";
transition: all var(--animation-speed-s1) ease;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: linear-gradient(0deg,hsla(0,0%,100%,0),hsla(0,0%,100%,.2));
opacity: 0;
}
/* Forms inputs */
/* ------------------------------------------------------------ */
/* Change Autocomplete styles in Chrome*/
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover
textarea:-webkit-autofill:focus,
textarea:-webkit-autofill:active,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus,
select:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0px 40px #FFF inset;
}
::-webkit-input-placeholder,
::-moz-placeholder,
:-ms-input-placeholder,
:-moz-placeholder {
color: var(--white);
}
.gm-floating-input {
position: relative;
}
.gm-floating-input input {
font-size: var(--text-base);
color: var(--grey-d3);
border: none;
border-radius: 0px;
border-bottom: 1px solid var(--grey-l1);
height: 38px;
-webkit-appearance: none;
box-sizing: border-box;
background: var(--white);
height: 44px;
width: 100%;
outline: none;
transition: border var(--animation-speed-f1) ease-in-out;
padding: 0 0 1px 26px; /* 1px bottom padding fixes jump that's caused by the border change */
letter-spacing: 0.2px;
}
.gm-floating-input input:hover {
border-bottom: 1px solid var(--grey);
}
.gm-floating-input input:focus {
border-bottom: 2px solid var(--blue);
padding: 0 0 0 26px;
}
.gm-floating-input input.gm-error {
border-bottom: 1px solid var(--red);
}
.gm-floating-input label {
display: flex;
align-items: center;
position: absolute;
font-size: var(--text-xs);
padding: 0 0 2px 0;
width: 100%;
top: 15px;
left: 24px;
color: var(--grey);
transition: all var(--animation-speed-base) ease-in-out;
transition-delay: 0.15s;
pointer-events: none;
text-transform: uppercase;
letter-spacing: 0.6px;
font-weight: 500;
}
.gm-floating-input input.gm-input-filled + label,
.gm-floating-input input:focus + label {
opacity: 0;
transition-delay: 0s;
}
.gm-floating-input label i svg {
width: 16px;
height: 16px;
}
.gm-floating-input label i svg path,
.gm-floating-input i svg path {
stroke: var(--grey);
transition: stroke var(--animation-speed-base) ease-in-out;
}
.gm-floating-input input.gm-input-filled + label + i svg path,
.gm-floating-input input:focus + label + i svg path{
stroke: var(--grey-d2);
}
.gm-floating-input i {
position: absolute;
top: 14px;
left: 0;
opacity: 1.0;
transition: all var(--animation-speed-f1) ease-in-out;
transition-delay: 0s;
}
.gm-floating-input input.gm-input-filled + label + i,
.gm-floating-input input:focus + label + i {
opacity: 1.0;
transform: translateX(0px);
transition-delay: 0.15s;
}
.gm-floating-input label i {
font-style: normal;
display: inline-block;
margin: 0 8px 0 0;
}
.gm-input-errortext {
color: var(--red);
font-size: var(--text-s);
letter-spacing: 0.4px;
margin: 4px 0 0;
font-weight: 500;
}
.gm-form-errortext {
color: var(--red);
font-size: var(--text-s);
letter-spacing: 0.4px;
margin: 28px -40px 0;
background: color-mod(var(--red) a(0.08));
padding: 12px 40px;
font-weight: 500;
display: flex;
justify-content: start;
align-items: center;
}
.gm-form-errortext i {
margin: 3px 8px 0 0;
}

View file

@ -0,0 +1,5 @@
@import './normalize.css';
@import './utils.css';
@import './variables.css';
@import './components.css';
@import './screen.css';

View file

@ -0,0 +1,341 @@
/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

View file

@ -0,0 +1,119 @@
/* Global styles */
/* ------------------------------------------------------------ */
html {
font-size: 62.5%;
}
html, body {
font-family: var(--default-font);
color: var(--black);
}
body {
font-size: var(--text-base);
letter-spacing: 0.2px;
}
p {
margin: 0;
line-height: 1.5em;
}
h1 {
margin: 0;
padding: 0;
color: var(--grey-d3);
font-size: var(--text-2xl);
font-weight: 700;
}
h4 {
margin: 0;
padding: 0;
color: var(--grey-d3);
font-size: var(--text-base);
}
a {
color: var(--blue);
transition: color var(--animation-speed-f1) ease-in-out;
cursor: pointer;
text-decoration: none;
}
a:hover {
color: var(--blue-d3);
}
@media (max-width: 500px) {
h1 {
font-size: var(--text-xl);
}
}
/* Auth Modal */
/* --------------------------------------------- */
.gm-logo {
width: 52px;
height: 52px;
border-radius: 4px;
background: #343F44 url('../assets/images/ghost-logo.svg') center center no-repeat;
}
.gm-auth-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin: 16px 0 0;
padding: 12px 0 0;
}
.gm-auth-header -cta {
padding: 0 0 3px;
}
.gm-auth-header h1 {
font-size: var(--text-xl);
}
.gm-auth-header h4 {
font-weight: normal;
font-size: var(--text-s);
letter-spacing: 0.4px;
color: var(--grey-d1);
}
.gm-auth-header a {
display: block;
font-size: var(--text-s);
letter-spacing: 0.4px;
padding: 8px;
margin: -8px -8px -8px -2px;
cursor: pointer;
color: var(--blue);
white-space: nowrap;
}
.gm-auth-header a:hover {
color: var(--blue-d3);
}
.gm-forgot-link {
position: absolute;
top: 14px;
right: 0;
z-index: 9999;
font-size: var(--text-s);
letter-spacing: 0.4px;
}
.gm-floating-input .gm-forgot-input {
padding-right: 60px;
}
@media (max-width: 440px) {
h4 {
display: none;
}
}

View file

@ -0,0 +1,638 @@
/* Layout Utitlities */
/* ------------------------------------------------------------
These are generic CSS classes that can be used on containers
that have the single purpose of setting up layout.
*/
/* Flexbox */
/* ------------------------------------------------------------ */
.flex { display: flex; }
.inline-flex { display: inline-flex; }
/* 1. Fix for Chrome 44 bug.
* https://code.google.com/p/chromium/issues/detail?id=506893 */
.flex-auto {
flex: 1 1 auto;
min-width: 0; /* 1 */
min-height: 0; /* 1 */
}
.flex-none { flex: none; }
.flex-column { flex-direction: column; }
.flex-row { flex-direction: row; }
.flex-wrap { flex-wrap: wrap; }
.flex-nowrap { flex-wrap: nowrap; }
.flex-wrap-reverse { flex-wrap: wrap-reverse; }
.flex-column-reverse { flex-direction: column-reverse; }
.flex-row-reverse { flex-direction: row-reverse; }
.items-start { align-items: flex-start; }
.items-end { align-items: flex-end; }
.items-center { align-items: center; }
.items-baseline { align-items: baseline; }
.items-stretch { align-items: stretch; }
.self-start { align-self: flex-start; }
.self-end { align-self: flex-end; }
.self-center { align-self: center; }
.self-baseline { align-self: baseline; }
.self-stretch { align-self: stretch; }
.justify-start { justify-content: flex-start; }
.justify-end { justify-content: flex-end; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.justify-around { justify-content: space-around; }
.content-start { align-content: flex-start; }
.content-end { align-content: flex-end; }
.content-center { align-content: center; }
.content-between { align-content: space-between; }
.content-around { align-content: space-around; }
.content-stretch { align-content: stretch; }
.order-0 { order: 0; }
.order-1 { order: 1; }
.order-2 { order: 2; }
.order-3 { order: 3; }
.order-4 { order: 4; }
.order-5 { order: 5; }
.order-6 { order: 6; }
.order-7 { order: 7; }
.order-8 { order: 8; }
.order-last { order: 99999; }
.flex-grow-0 { flex-grow: 0; }
.flex-grow-1 { flex-grow: 1; }
.flex-shrink-0 { flex-shrink: 0; }
.flex-shrink-1 { flex-shrink: 1; }
/* Margins and paddings */
/* ------------------------------------------------------------ */
:root {
--grid-size: 0.4rem;
}
.pa0 { padding: 0; }
.pa1 { padding: calc(var(--grid-size) * 1); }
.pa2 { padding: calc(var(--grid-size) * 2); }
.pa3 { padding: calc(var(--grid-size) * 3); }
.pa4 { padding: calc(var(--grid-size) * 4); }
.pa5 { padding: calc(var(--grid-size) * 5); }
.pa6 { padding: calc(var(--grid-size) * 6); }
.pa7 { padding: calc(var(--grid-size) * 7); }
.pa8 { padding: calc(var(--grid-size) * 8); }
.pa9 { padding: calc(var(--grid-size) * 9); }
.pa10 { padding: calc(var(--grid-size) * 10); }
.pa11 { padding: calc(var(--grid-size) * 11); }
.pa12 { padding: calc(var(--grid-size) * 12); }
.pa13 { padding: calc(var(--grid-size) * 13); }
.pa14 { padding: calc(var(--grid-size) * 14); }
.pa15 { padding: calc(var(--grid-size) * 15); }
.pa16 { padding: calc(var(--grid-size) * 16); }
.pa17 { padding: calc(var(--grid-size) * 17); }
.pa18 { padding: calc(var(--grid-size) * 18); }
.pa19 { padding: calc(var(--grid-size) * 19); }
.pa20 { padding: calc(var(--grid-size) * 20); }
.pa25 { padding: calc(var(--grid-size) * 25); }
.pa30 { padding: calc(var(--grid-size) * 30); }
.pa40 { padding: calc(var(--grid-size) * 40); }
.pa50 { padding: calc(var(--grid-size) * 50); }
.pr0 { padding-right: 0; }
.pr1 { padding-right: calc(var(--grid-size) * 1); }
.pr2 { padding-right: calc(var(--grid-size) * 2); }
.pr3 { padding-right: calc(var(--grid-size) * 3); }
.pr4 { padding-right: calc(var(--grid-size) * 4); }
.pr5 { padding-right: calc(var(--grid-size) * 5); }
.pr6 { padding-right: calc(var(--grid-size) * 6); }
.pr7 { padding-right: calc(var(--grid-size) * 7); }
.pr8 { padding-right: calc(var(--grid-size) * 8); }
.pr9 { padding-right: calc(var(--grid-size) * 9); }
.pr10 { padding-right: calc(var(--grid-size) * 10); }
.pr11 { padding-right: calc(var(--grid-size) * 11); }
.pr12 { padding-right: calc(var(--grid-size) * 12); }
.pr13 { padding-right: calc(var(--grid-size) * 13); }
.pr14 { padding-right: calc(var(--grid-size) * 14); }
.pr15 { padding-right: calc(var(--grid-size) * 15); }
.pr16 { padding-right: calc(var(--grid-size) * 16); }
.pr17 { padding-right: calc(var(--grid-size) * 17); }
.pr18 { padding-right: calc(var(--grid-size) * 18); }
.pr19 { padding-right: calc(var(--grid-size) * 19); }
.pr20 { padding-right: calc(var(--grid-size) * 20); }
.pr25 { padding-right: calc(var(--grid-size) * 25); }
.pr30 { padding-right: calc(var(--grid-size) * 30); }
.pr40 { padding-right: calc(var(--grid-size) * 40); }
.pr50 { padding-right: calc(var(--grid-size) * 50); }
.pb0 { padding-bottom: 0; }
.pb1 { padding-bottom: calc(var(--grid-size) * 1); }
.pb2 { padding-bottom: calc(var(--grid-size) * 2); }
.pb3 { padding-bottom: calc(var(--grid-size) * 3); }
.pb4 { padding-bottom: calc(var(--grid-size) * 4); }
.pb5 { padding-bottom: calc(var(--grid-size) * 5); }
.pb6 { padding-bottom: calc(var(--grid-size) * 6); }
.pb7 { padding-bottom: calc(var(--grid-size) * 7); }
.pb8 { padding-bottom: calc(var(--grid-size) * 8); }
.pb9 { padding-bottom: calc(var(--grid-size) * 9); }
.pb10 { padding-bottom: calc(var(--grid-size) * 10); }
.pb11 { padding-bottom: calc(var(--grid-size) * 11); }
.pb12 { padding-bottom: calc(var(--grid-size) * 12); }
.pb13 { padding-bottom: calc(var(--grid-size) * 13); }
.pb14 { padding-bottom: calc(var(--grid-size) * 14); }
.pb15 { padding-bottom: calc(var(--grid-size) * 15); }
.pb16 { padding-bottom: calc(var(--grid-size) * 16); }
.pb17 { padding-bottom: calc(var(--grid-size) * 17); }
.pb18 { padding-bottom: calc(var(--grid-size) * 18); }
.pb19 { padding-bottom: calc(var(--grid-size) * 19); }
.pb20 { padding-bottom: calc(var(--grid-size) * 20); }
.pb25 { padding-bottom: calc(var(--grid-size) * 25); }
.pb30 { padding-bottom: calc(var(--grid-size) * 30); }
.pb40 { padding-bottom: calc(var(--grid-size) * 40); }
.pb50 { padding-bottom: calc(var(--grid-size) * 50); }
.pl0 { padding-left: 0; }
.pl1 { padding-left: calc(var(--grid-size) * 1); }
.pl2 { padding-left: calc(var(--grid-size) * 2); }
.pl3 { padding-left: calc(var(--grid-size) * 3); }
.pl4 { padding-left: calc(var(--grid-size) * 4); }
.pl5 { padding-left: calc(var(--grid-size) * 5); }
.pl6 { padding-left: calc(var(--grid-size) * 6); }
.pl7 { padding-left: calc(var(--grid-size) * 7); }
.pl8 { padding-left: calc(var(--grid-size) * 8); }
.pl9 { padding-left: calc(var(--grid-size) * 9); }
.pl10 { padding-left: calc(var(--grid-size) * 10); }
.pl11 { padding-left: calc(var(--grid-size) * 11); }
.pl12 { padding-left: calc(var(--grid-size) * 12); }
.pl13 { padding-left: calc(var(--grid-size) * 13); }
.pl14 { padding-left: calc(var(--grid-size) * 14); }
.pl15 { padding-left: calc(var(--grid-size) * 15); }
.pl16 { padding-left: calc(var(--grid-size) * 16); }
.pl17 { padding-left: calc(var(--grid-size) * 17); }
.pl18 { padding-left: calc(var(--grid-size) * 18); }
.pl19 { padding-left: calc(var(--grid-size) * 19); }
.pl20 { padding-left: calc(var(--grid-size) * 20); }
.pl25 { padding-left: calc(var(--grid-size) * 25); }
.pl30 { padding-left: calc(var(--grid-size) * 30); }
.pl40 { padding-left: calc(var(--grid-size) * 40); }
.pl50 { padding-left: calc(var(--grid-size) * 50); }
.pt0 { padding-top: 0; }
.pt1 { padding-top: calc(var(--grid-size) * 1); }
.pt2 { padding-top: calc(var(--grid-size) * 2); }
.pt3 { padding-top: calc(var(--grid-size) * 3); }
.pt4 { padding-top: calc(var(--grid-size) * 4); }
.pt5 { padding-top: calc(var(--grid-size) * 5); }
.pt6 { padding-top: calc(var(--grid-size) * 6); }
.pt7 { padding-top: calc(var(--grid-size) * 7); }
.pt8 { padding-top: calc(var(--grid-size) * 8); }
.pt9 { padding-top: calc(var(--grid-size) * 9); }
.pt10 { padding-top: calc(var(--grid-size) * 10); }
.pt11 { padding-top: calc(var(--grid-size) * 11); }
.pt12 { padding-top: calc(var(--grid-size) * 12); }
.pt13 { padding-top: calc(var(--grid-size) * 13); }
.pt14 { padding-top: calc(var(--grid-size) * 14); }
.pt15 { padding-top: calc(var(--grid-size) * 15); }
.pt16 { padding-top: calc(var(--grid-size) * 16); }
.pt17 { padding-top: calc(var(--grid-size) * 17); }
.pt18 { padding-top: calc(var(--grid-size) * 18); }
.pt19 { padding-top: calc(var(--grid-size) * 19); }
.pt20 { padding-top: calc(var(--grid-size) * 20); }
.pt25 { padding-top: calc(var(--grid-size) * 25); }
.pt30 { padding-top: calc(var(--grid-size) * 30); }
.pt40 { padding-top: calc(var(--grid-size) * 40); }
.pt50 { padding-top: calc(var(--grid-size) * 50); }
.ma0 { margin: 0; }
.ma1 { margin: calc(var(--grid-size) * 1); }
.ma2 { margin: calc(var(--grid-size) * 2); }
.ma3 { margin: calc(var(--grid-size) * 3); }
.ma4 { margin: calc(var(--grid-size) * 4); }
.ma5 { margin: calc(var(--grid-size) * 5); }
.ma6 { margin: calc(var(--grid-size) * 6); }
.ma7 { margin: calc(var(--grid-size) * 7); }
.ma8 { margin: calc(var(--grid-size) * 8); }
.ma9 { margin: calc(var(--grid-size) * 9); }
.ma10 { margin: calc(var(--grid-size) * 10); }
.ma11 { margin: calc(var(--grid-size) * 11); }
.ma12 { margin: calc(var(--grid-size) * 12); }
.ma13 { margin: calc(var(--grid-size) * 13); }
.ma14 { margin: calc(var(--grid-size) * 14); }
.ma15 { margin: calc(var(--grid-size) * 15); }
.ma16 { margin: calc(var(--grid-size) * 16); }
.ma17 { margin: calc(var(--grid-size) * 17); }
.ma18 { margin: calc(var(--grid-size) * 18); }
.ma19 { margin: calc(var(--grid-size) * 19); }
.ma20 { margin: calc(var(--grid-size) * 20); }
.ma25 { margin: calc(var(--grid-size) * 25); }
.ma30 { margin: calc(var(--grid-size) * 30); }
.ma40 { margin: calc(var(--grid-size) * 40); }
.ma50 { margin: calc(var(--grid-size) * 50); }
.mr0 { margin-right: 0; }
.mr1 { margin-right: calc(var(--grid-size) * 1); }
.mr2 { margin-right: calc(var(--grid-size) * 2); }
.mr3 { margin-right: calc(var(--grid-size) * 3); }
.mr4 { margin-right: calc(var(--grid-size) * 4); }
.mr5 { margin-right: calc(var(--grid-size) * 5); }
.mr6 { margin-right: calc(var(--grid-size) * 6); }
.mr7 { margin-right: calc(var(--grid-size) * 7); }
.mr8 { margin-right: calc(var(--grid-size) * 8); }
.mr9 { margin-right: calc(var(--grid-size) * 9); }
.mr10 { margin-right: calc(var(--grid-size) * 10); }
.mr11 { margin-right: calc(var(--grid-size) * 11); }
.mr12 { margin-right: calc(var(--grid-size) * 12); }
.mr13 { margin-right: calc(var(--grid-size) * 13); }
.mr14 { margin-right: calc(var(--grid-size) * 14); }
.mr15 { margin-right: calc(var(--grid-size) * 15); }
.mr16 { margin-right: calc(var(--grid-size) * 16); }
.mr17 { margin-right: calc(var(--grid-size) * 17); }
.mr18 { margin-right: calc(var(--grid-size) * 18); }
.mr19 { margin-right: calc(var(--grid-size) * 19); }
.mr20 { margin-right: calc(var(--grid-size) * 20); }
.mr25 { margin-right: calc(var(--grid-size) * 25); }
.mr30 { margin-right: calc(var(--grid-size) * 30); }
.mr40 { margin-right: calc(var(--grid-size) * 40); }
.mr50 { margin-right: calc(var(--grid-size) * 50); }
.mb0 { margin-bottom: 0; }
.mb1 { margin-bottom: calc(var(--grid-size) * 1); }
.mb2 { margin-bottom: calc(var(--grid-size) * 2); }
.mb3 { margin-bottom: calc(var(--grid-size) * 3); }
.mb4 { margin-bottom: calc(var(--grid-size) * 4); }
.mb5 { margin-bottom: calc(var(--grid-size) * 5); }
.mb6 { margin-bottom: calc(var(--grid-size) * 6); }
.mb7 { margin-bottom: calc(var(--grid-size) * 7); }
.mb8 { margin-bottom: calc(var(--grid-size) * 8); }
.mb9 { margin-bottom: calc(var(--grid-size) * 9); }
.mb10 { margin-bottom: calc(var(--grid-size) * 10); }
.mb11 { margin-bottom: calc(var(--grid-size) * 11); }
.mb12 { margin-bottom: calc(var(--grid-size) * 12); }
.mb13 { margin-bottom: calc(var(--grid-size) * 13); }
.mb14 { margin-bottom: calc(var(--grid-size) * 14); }
.mb15 { margin-bottom: calc(var(--grid-size) * 15); }
.mb16 { margin-bottom: calc(var(--grid-size) * 16); }
.mb17 { margin-bottom: calc(var(--grid-size) * 17); }
.mb18 { margin-bottom: calc(var(--grid-size) * 18); }
.mb19 { margin-bottom: calc(var(--grid-size) * 19); }
.mb20 { margin-bottom: calc(var(--grid-size) * 20); }
.mb25 { margin-bottom: calc(var(--grid-size) * 25); }
.mb30 { margin-bottom: calc(var(--grid-size) * 30); }
.mb40 { margin-bottom: calc(var(--grid-size) * 40); }
.mb50 { margin-bottom: calc(var(--grid-size) * 50); }
.ml0 { margin-left: 0; }
.ml1 { margin-left: calc(var(--grid-size) * 1); }
.ml2 { margin-left: calc(var(--grid-size) * 2); }
.ml3 { margin-left: calc(var(--grid-size) * 3); }
.ml4 { margin-left: calc(var(--grid-size) * 4); }
.ml5 { margin-left: calc(var(--grid-size) * 5); }
.ml6 { margin-left: calc(var(--grid-size) * 6); }
.ml7 { margin-left: calc(var(--grid-size) * 7); }
.ml8 { margin-left: calc(var(--grid-size) * 8); }
.ml9 { margin-left: calc(var(--grid-size) * 9); }
.ml10 { margin-left: calc(var(--grid-size) * 10); }
.ml11 { margin-left: calc(var(--grid-size) * 11); }
.ml12 { margin-left: calc(var(--grid-size) * 12); }
.ml13 { margin-left: calc(var(--grid-size) * 13); }
.ml14 { margin-left: calc(var(--grid-size) * 14); }
.ml15 { margin-left: calc(var(--grid-size) * 15); }
.ml16 { margin-left: calc(var(--grid-size) * 16); }
.ml17 { margin-left: calc(var(--grid-size) * 17); }
.ml18 { margin-left: calc(var(--grid-size) * 18); }
.ml19 { margin-left: calc(var(--grid-size) * 19); }
.ml20 { margin-left: calc(var(--grid-size) * 20); }
.ml25 { margin-left: calc(var(--grid-size) * 25); }
.ml30 { margin-left: calc(var(--grid-size) * 30); }
.ml40 { margin-left: calc(var(--grid-size) * 40); }
.ml50 { margin-left: calc(var(--grid-size) * 50); }
.mt0 { margin-top: 0; }
.mt1 { margin-top: calc(var(--grid-size) * 1); }
.mt2 { margin-top: calc(var(--grid-size) * 2); }
.mt3 { margin-top: calc(var(--grid-size) * 3); }
.mt4 { margin-top: calc(var(--grid-size) * 4); }
.mt5 { margin-top: calc(var(--grid-size) * 5); }
.mt6 { margin-top: calc(var(--grid-size) * 6); }
.mt7 { margin-top: calc(var(--grid-size) * 7); }
.mt8 { margin-top: calc(var(--grid-size) * 8); }
.mt9 { margin-top: calc(var(--grid-size) * 9); }
.mt10 { margin-top: calc(var(--grid-size) * 10); }
.mt11 { margin-top: calc(var(--grid-size) * 11); }
.mt12 { margin-top: calc(var(--grid-size) * 12); }
.mt13 { margin-top: calc(var(--grid-size) * 13); }
.mt14 { margin-top: calc(var(--grid-size) * 14); }
.mt15 { margin-top: calc(var(--grid-size) * 15); }
.mt16 { margin-top: calc(var(--grid-size) * 16); }
.mt17 { margin-top: calc(var(--grid-size) * 17); }
.mt18 { margin-top: calc(var(--grid-size) * 18); }
.mt19 { margin-top: calc(var(--grid-size) * 19); }
.mt20 { margin-top: calc(var(--grid-size) * 20); }
.mt25 { margin-top: calc(var(--grid-size) * 25); }
.mt30 { margin-top: calc(var(--grid-size) * 30); }
.mt40 { margin-top: calc(var(--grid-size) * 40); }
.mt50 { margin-top: calc(var(--grid-size) * 50); }
.na0 { margin: 0; }
.na1 { margin: calc(-1 * var(--grid-size) * 1); }
.na2 { margin: calc(-1 * var(--grid-size) * 2); }
.na3 { margin: calc(-1 * var(--grid-size) * 3); }
.na4 { margin: calc(-1 * var(--grid-size) * 4); }
.na5 { margin: calc(-1 * var(--grid-size) * 5); }
.na6 { margin: calc(-1 * var(--grid-size) * 6); }
.na7 { margin: calc(-1 * var(--grid-size) * 7); }
.na8 { margin: calc(-1 * var(--grid-size) * 8); }
.na9 { margin: calc(-1 * var(--grid-size) * 9); }
.na10 { margin: calc(-1 * var(--grid-size) * 10); }
.na11 { margin: calc(-1 * var(--grid-size) * 11); }
.na12 { margin: calc(-1 * var(--grid-size) * 12); }
.na13 { margin: calc(-1 * var(--grid-size) * 13); }
.na14 { margin: calc(-1 * var(--grid-size) * 14); }
.na15 { margin: calc(-1 * var(--grid-size) * 15); }
.na16 { margin: calc(-1 * var(--grid-size) * 16); }
.na17 { margin: calc(-1 * var(--grid-size) * 17); }
.na18 { margin: calc(-1 * var(--grid-size) * 18); }
.na19 { margin: calc(-1 * var(--grid-size) * 19); }
.na20 { margin: calc(-1 * var(--grid-size) * 20); }
.na25 { margin: calc(-1 * var(--grid-size) * 25); }
.na30 { margin: calc(-1 * var(--grid-size) * 30); }
.na40 { margin: calc(-1 * var(--grid-size) * 40); }
.na50 { margin: calc(-1 * var(--grid-size) * 50); }
.nr0 { margin-right: 0; }
.nr1 { margin-right: calc(-1 * var(--grid-size) * 1); }
.nr2 { margin-right: calc(-1 * var(--grid-size) * 2); }
.nr3 { margin-right: calc(-1 * var(--grid-size) * 3); }
.nr4 { margin-right: calc(-1 * var(--grid-size) * 4); }
.nr5 { margin-right: calc(-1 * var(--grid-size) * 5); }
.nr6 { margin-right: calc(-1 * var(--grid-size) * 6); }
.nr7 { margin-right: calc(-1 * var(--grid-size) * 7); }
.nr8 { margin-right: calc(-1 * var(--grid-size) * 8); }
.nr9 { margin-right: calc(-1 * var(--grid-size) * 9); }
.nr10 { margin-right: calc(-1 * var(--grid-size) * 10); }
.nr11 { margin-right: calc(-1 * var(--grid-size) * 11); }
.nr12 { margin-right: calc(-1 * var(--grid-size) * 12); }
.nr13 { margin-right: calc(-1 * var(--grid-size) * 13); }
.nr14 { margin-right: calc(-1 * var(--grid-size) * 14); }
.nr15 { margin-right: calc(-1 * var(--grid-size) * 15); }
.nr16 { margin-right: calc(-1 * var(--grid-size) * 16); }
.nr17 { margin-right: calc(-1 * var(--grid-size) * 17); }
.nr18 { margin-right: calc(-1 * var(--grid-size) * 18); }
.nr19 { margin-right: calc(-1 * var(--grid-size) * 19); }
.nr20 { margin-right: calc(-1 * var(--grid-size) * 20); }
.nr25 { margin-right: calc(-1 * var(--grid-size) * 25); }
.nr30 { margin-right: calc(-1 * var(--grid-size) * 30); }
.nr40 { margin-right: calc(-1 * var(--grid-size) * 40); }
.nr50 { margin-right: calc(-1 * var(--grid-size) * 50); }
.nb0 { margin-bottom: 0; }
.nb1 { margin-bottom: calc(-1 * var(--grid-size) * 1); }
.nb2 { margin-bottom: calc(-1 * var(--grid-size) * 2); }
.nb3 { margin-bottom: calc(-1 * var(--grid-size) * 3); }
.nb4 { margin-bottom: calc(-1 * var(--grid-size) * 4); }
.nb5 { margin-bottom: calc(-1 * var(--grid-size) * 5); }
.nb6 { margin-bottom: calc(-1 * var(--grid-size) * 6); }
.nb7 { margin-bottom: calc(-1 * var(--grid-size) * 7); }
.nb8 { margin-bottom: calc(-1 * var(--grid-size) * 8); }
.nb9 { margin-bottom: calc(-1 * var(--grid-size) * 9); }
.nb10 { margin-bottom: calc(-1 * var(--grid-size) * 10); }
.nb11 { margin-bottom: calc(-1 * var(--grid-size) * 11); }
.nb12 { margin-bottom: calc(-1 * var(--grid-size) * 12); }
.nb13 { margin-bottom: calc(-1 * var(--grid-size) * 13); }
.nb14 { margin-bottom: calc(-1 * var(--grid-size) * 14); }
.nb15 { margin-bottom: calc(-1 * var(--grid-size) * 15); }
.nb16 { margin-bottom: calc(-1 * var(--grid-size) * 16); }
.nb17 { margin-bottom: calc(-1 * var(--grid-size) * 17); }
.nb18 { margin-bottom: calc(-1 * var(--grid-size) * 18); }
.nb19 { margin-bottom: calc(-1 * var(--grid-size) * 19); }
.nb20 { margin-bottom: calc(-1 * var(--grid-size) * 20); }
.nb25 { margin-bottom: calc(-1 * var(--grid-size) * 25); }
.nb30 { margin-bottom: calc(-1 * var(--grid-size) * 30); }
.nb40 { margin-bottom: calc(-1 * var(--grid-size) * 40); }
.nb50 { margin-bottom: calc(-1 * var(--grid-size) * 50); }
.nl0 { margin-left: 0; }
.nl1 { margin-left: calc(-1 * var(--grid-size) * 1); }
.nl2 { margin-left: calc(-1 * var(--grid-size) * 2); }
.nl3 { margin-left: calc(-1 * var(--grid-size) * 3); }
.nl4 { margin-left: calc(-1 * var(--grid-size) * 4); }
.nl5 { margin-left: calc(-1 * var(--grid-size) * 5); }
.nl6 { margin-left: calc(-1 * var(--grid-size) * 6); }
.nl7 { margin-left: calc(-1 * var(--grid-size) * 7); }
.nl8 { margin-left: calc(-1 * var(--grid-size) * 8); }
.nl9 { margin-left: calc(-1 * var(--grid-size) * 9); }
.nl10 { margin-left: calc(-1 * var(--grid-size) * 10); }
.nl11 { margin-left: calc(-1 * var(--grid-size) * 11); }
.nl12 { margin-left: calc(-1 * var(--grid-size) * 12); }
.nl13 { margin-left: calc(-1 * var(--grid-size) * 13); }
.nl14 { margin-left: calc(-1 * var(--grid-size) * 14); }
.nl15 { margin-left: calc(-1 * var(--grid-size) * 15); }
.nl16 { margin-left: calc(-1 * var(--grid-size) * 16); }
.nl17 { margin-left: calc(-1 * var(--grid-size) * 17); }
.nl18 { margin-left: calc(-1 * var(--grid-size) * 18); }
.nl19 { margin-left: calc(-1 * var(--grid-size) * 19); }
.nl20 { margin-left: calc(-1 * var(--grid-size) * 20); }
.nl25 { margin-left: calc(-1 * var(--grid-size) * 25); }
.nl30 { margin-left: calc(-1 * var(--grid-size) * 30); }
.nl40 { margin-left: calc(-1 * var(--grid-size) * 40); }
.nl50 { margin-left: calc(-1 * var(--grid-size) * 50); }
.nt0 { margin-top: 0; }
.nt1 { margin-top: calc(-1 * var(--grid-size) * 1); }
.nt2 { margin-top: calc(-1 * var(--grid-size) * 2); }
.nt3 { margin-top: calc(-1 * var(--grid-size) * 3); }
.nt4 { margin-top: calc(-1 * var(--grid-size) * 4); }
.nt5 { margin-top: calc(-1 * var(--grid-size) * 5); }
.nt6 { margin-top: calc(-1 * var(--grid-size) * 6); }
.nt7 { margin-top: calc(-1 * var(--grid-size) * 7); }
.nt8 { margin-top: calc(-1 * var(--grid-size) * 8); }
.nt9 { margin-top: calc(-1 * var(--grid-size) * 9); }
.nt10 { margin-top: calc(-1 * var(--grid-size) * 10); }
.nt11 { margin-top: calc(-1 * var(--grid-size) * 11); }
.nt12 { margin-top: calc(-1 * var(--grid-size) * 12); }
.nt13 { margin-top: calc(-1 * var(--grid-size) * 13); }
.nt14 { margin-top: calc(-1 * var(--grid-size) * 14); }
.nt15 { margin-top: calc(-1 * var(--grid-size) * 15); }
.nt16 { margin-top: calc(-1 * var(--grid-size) * 16); }
.nt17 { margin-top: calc(-1 * var(--grid-size) * 17); }
.nt18 { margin-top: calc(-1 * var(--grid-size) * 18); }
.nt19 { margin-top: calc(-1 * var(--grid-size) * 19); }
.nt20 { margin-top: calc(-1 * var(--grid-size) * 20); }
.nt25 { margin-top: calc(-1 * var(--grid-size) * 25); }
.nt30 { margin-top: calc(-1 * var(--grid-size) * 30); }
.nt40 { margin-top: calc(-1 * var(--grid-size) * 40); }
.nt50 { margin-top: calc(-1 * var(--grid-size) * 50); }
/* Nudging */
/* ------------------------------------------------------------ */
.nudge-top--1 {
position: relative;
top: 1px;
}
.nudge-top--2 {
position: relative;
top: 2px;
}
.nudge-top--3 {
position: relative;
top: 3px;
}
.nudge-top--4 {
position: relative;
top: 4px;
}
.nudge-top--5 {
position: relative;
top: 5px;
}
.nudge-top--6 {
position: relative;
top: 6px;
}
.nudge-top--7 {
position: relative;
top: 7px;
}
.nudge-top--8 {
position: relative;
top: 8px;
}
.nudge-top--9 {
position: relative;
top: 9px;
}
.nudge-top--10 {
position: relative;
top: 10px;
}
.nudge-right--1 {
position: relative;
right: 1px;
}
.nudge-right--2 {
position: relative;
right: 2px;
}
.nudge-right--3 {
position: relative;
right: 3px;
}
.nudge-right--4 {
position: relative;
right: 4px;
}
.nudge-right--5 {
position: relative;
right: 5px;
}
.nudge-right--6 {
position: relative;
right: 6px;
}
.nudge-right--7 {
position: relative;
right: 7px;
}
.nudge-right--8 {
position: relative;
right: 8px;
}
.nudge-right--9 {
position: relative;
right: 9px;
}
.nudge-right--10 {
position: relative;
right: 10px;
}
.nudge-bottom--1 {
position: relative;
bottom: 1px;
}
.nudge-bottom--2 {
position: relative;
bottom: 2px;
}
.nudge-bottom--3 {
position: relative;
bottom: 3px;
}
.nudge-bottom--4 {
position: relative;
bottom: 4px;
}
.nudge-bottom--5 {
position: relative;
bottom: 5px;
}
.nudge-bottom--6 {
position: relative;
bottom: 6px;
}
.nudge-bottom--7 {
position: relative;
bottom: 7px;
}
.nudge-bottom--8 {
position: relative;
bottom: 8px;
}
.nudge-bottom--9 {
position: relative;
bottom: 9px;
}
.nudge-bottom--10 {
position: relative;
bottom: 10px;
}
.nudge-left--1 {
position: relative;
left: 1px;
}
.nudge-left--2 {
position: relative;
left: 2px;
}
.nudge-left--3 {
position: relative;
left: 3px;
}
.nudge-left--4 {
position: relative;
left: 4px;
}
.nudge-left--5 {
position: relative;
left: 5px;
}
.nudge-left--6 {
position: relative;
left: 6px;
}
.nudge-left--7 {
position: relative;
left: 7px;
}
.nudge-left--8 {
position: relative;
left: 8px;
}
.nudge-left--9 {
position: relative;
left: 9px;
}
.nudge-left--10 {
position: relative;
left: 10px;
}

View file

@ -0,0 +1,110 @@
/* Design system variables */
/* ------------------------------------------------------------
Variables to define colors, fonts and various visual elements.
Layout use a 4px grid.
*/
/* Colors */
/* ------------------------------------------------------------ */
:root {
/* Base colors */
--blue: #3eb0ef;
--green: #a4d037;
--red: #f05230;
--yellow: #fecd35;
--white: #ffffff;
--grey: #B8C2CC;
--black: #22292F;
/* Variations */
--blue-l3: color-mod(var(--blue) l(+15%));
--blue-l2: color-mod(var(--blue) l(+10%));
--blue-l1: color-mod(var(--blue) l(+5%));
--blue-d1: color-mod(var(--blue) l(-5%));
--blue-d2: color-mod(var(--blue) l(-10%));
--blue-d3: color-mod(var(--blue) l(-15%));
--green-l3: color-mod(var(--green) l(+15%));
--green-l2: color-mod(var(--green) l(+10%));
--green-l1: color-mod(var(--green) l(+5%));
--green-d1: color-mod(var(--green) l(-5%));
--green-d2: color-mod(var(--green) l(-10%));
--green-d3: color-mod(var(--green) l(-15%));
--yellow-l3: color-mod(var(--yellow) l(+15%));
--yellow-l2: color-mod(var(--yellow) l(+10%));
--yellow-l1: color-mod(var(--yellow) l(+5%));
--yellow-d1: color-mod(var(--yellow) l(-5%));
--yellow-d2: color-mod(var(--yellow) l(-10%));
--yellow-d3: color-mod(var(--yellow) l(-13%));
--red-l3: color-mod(var(--red) l(+15%));
--red-l2: color-mod(var(--red) l(+10%));
--red-l1: color-mod(var(--red) l(+5%));
--red-d1: color-mod(var(--red) l(-5%));
--red-d2: color-mod(var(--red) l(-10%));
--red-d3: color-mod(var(--red) l(-15%));
--grey-l3:#F8FAFC;
--grey-l2:#F1F5F8;
--grey-l1:#DAE1E7;
--grey-d1:#8795A1;
--grey-d2:#606F7B;
--grey-d3:#3D4852;
}
/* Typography */
/* ------------------------------------------------------------ */
/* Fonts */
:root {
--default-font: -apple-system, BlinkMacSystemFont,
'avenir next', avenir,
'helvetica neue', helvetica,
ubuntu,
roboto, noto,
'segoe ui', arial,
sans-serif;
}
/* Type scale */
:root {
--text-2xs: 1.15rem;
--text-xs: 1.3rem;
--text-s: 1.4rem;
--text-base: 1.5rem;
--text-l: 1.8rem;
--text-xl: 2.5rem;
--text-2xl: 3.0rem;
--text-3xl: 3.6rem;
--text-4xl: 4.5rem;
}
/* Visual elements */
/* ------------------------------------------------------------ */
/* Borders */
:root {
--border-radius-s: 2px;
--border-radius-base: 4px;
--border-radius-l: 8px;
--border-radius-xl: 12px;
}
/* Shadows */
:root {
--box-shadow-base: 0 0 1px rgba(0,0,0,.12), 0 16px 24px -12px rgba(0,0,0,.2);
}
/* Animations */
:root {
/* Speed (f: faster, s: slower) */
--animation-speed-f1: 0.18s;
--animation-speed-base: 0.25s;
--animation-speed-s1: 0.45s;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,184 @@
/* global window document location fetch */
(function () {
if (window.parent === window) {
return;
}
let storage;
try {
storage = window.localStorage;
} catch (e) {
storage = window.sessionStorage;
}
const origin = new URL(document.referrer).origin;
const handlers = {};
function addMethod(method, fn) {
handlers[method] = function ({uid, options}) {
fn(options)
.then(function (data) {
window.parent.postMessage({uid, data}, origin);
})
.catch(function (error) {
window.parent.postMessage({uid, error: error.message}, origin);
});
};
}
// @TODO this needs to be configurable
const membersApi = location.pathname.replace(/\/members\/gateway\/?$/, '/ghost/api/v2/members');
function getToken({audience}) {
return fetch(`${membersApi}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
origin,
audience: audience || origin
})
}).then((res) => {
if (!res.ok) {
if (res.status === 401) {
storage.removeItem('signedin');
}
return null;
}
storage.setItem('signedin', true);
return res.text();
});
}
addMethod('init', function init() {
if (storage.getItem('signedin')) {
window.parent.postMessage({event: 'signedin'}, origin);
} else {
window.parent.postMessage({event: 'signedout'}, origin);
}
getToken({audience: origin});
return Promise.resolve();
});
addMethod('getToken', getToken);
addMethod('signin', function signin({email, password}) {
return fetch(`${membersApi}/signin`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
origin,
email,
password
})
}).then((res) => {
if (res.ok) {
storage.setItem('signedin', true);
}
return res.ok;
});
});
addMethod('signup', function signin({name, email, password}) {
return fetch(`${membersApi}/signup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
origin,
name,
email,
password
})
}).then((res) => {
if (res.ok) {
storage.setItem('signedin', true);
}
return res.ok;
});
});
addMethod('signout', function signout(/*options*/) {
return fetch(`${membersApi}/signout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
origin
})
}).then((res) => {
if (res.ok) {
storage.removeItem('signedin');
}
return res.ok;
});
});
addMethod('request-password-reset', function signout({email}) {
return fetch(`${membersApi}/request-password-reset`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
origin,
email
})
}).then((res) => {
return res.ok;
});
});
addMethod('reset-password', function signout({token, password}) {
return fetch(`${membersApi}/reset-password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
origin,
token,
password
})
}).then((res) => {
if (res.ok) {
storage.setItem('signedin', true);
}
return res.ok;
});
});
window.addEventListener('storage', function (event) {
if (event.storageArea !== storage) {
return;
}
const newValue = event.newValue;
const oldValue = event.oldValue;
if (event.key === 'signedin') {
if (newValue && !oldValue) {
return window.parent.postMessage({event: 'signedin'}, origin);
}
if (!newValue && oldValue) {
return window.parent.postMessage({event: 'signedout'}, origin);
}
}
});
window.addEventListener('message', function (event) {
if (event.origin !== origin) {
return;
}
if (!event.data || !event.data.uid) {
return;
}
if (!handlers[event.data.method]) {
return window.parent.postMessage({
uid: event.data.uid,
error: 'Unknown method'
}, origin);
}
handlers[event.data.method](event.data);
});
})();

View file

@ -0,0 +1 @@
<script src="bundle.js"></script>