Merge branch 'master' into smart-links

This commit is contained in:
Baltazar Gomez 2019-06-09 01:00:44 -06:00 committed by GitHub
commit 05e4f87707
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 243 additions and 242 deletions

View file

@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [0.33.0] - [Unreleased]
### Fixed
- Channel page styling ([#2520](https://github.com/lbryio/lbry-desktop/pull/2520))
### Added
- Comic book reader ([#2484](https://github.com/lbryio/lbry-desktop/pull/2484))
- Base functionality for more language support ([#2495](https://github.com/lbryio/lbry-desktop/pull2495))
- Add easy thumbnail selector for videos ([#2492](https://github.com/lbryio/lbry-desktop/pull2492))
## [0.32.2] - [2019-5-20]
### Fixed

View file

@ -24,14 +24,13 @@ const downloadDaemon = targetPlatform =>
const daemonFilePath = path.join(daemonDir, daemonFileName);
const daemonVersionPath = path.join(__dirname, 'daemon.ver');
const tmpZipPath = path.join(__dirname, '..', 'dist', 'daemon.zip');
const daemonURL = daemonURLTemplate
.replace(/DAEMONVER/g, daemonVersion)
.replace(/OSNAME/g, daemonPlatform);
const daemonURL = daemonURLTemplate.replace(/DAEMONVER/g, daemonVersion).replace(/OSNAME/g, daemonPlatform);
// If a daemon and daemon.ver exists, check to see if it matches the current daemon version
const hasDaemonDownloaded = fs.existsSync(daemonFilePath);
const hasDaemonVersion = fs.existsSync(daemonVersionPath);
let downloadedDaemonVersion;
if (hasDaemonVersion) {
downloadedDaemonVersion = fs.readFileSync(daemonVersionPath, 'utf8');
}
@ -65,6 +64,7 @@ const downloadDaemon = targetPlatform =>
})
)
.then(() => del(`${daemonFilePath}*`))
.then()
.then(() =>
decompress(tmpZipPath, daemonDir, {
filter: file => path.basename(file.path) === daemonFileName,
@ -80,9 +80,7 @@ const downloadDaemon = targetPlatform =>
resolve('Done');
})
.catch(error => {
console.error(
`\x1b[31merror\x1b[0m Daemon download failed due to: \x1b[35m${error}\x1b[0m`
);
console.error(`\x1b[31merror\x1b[0m Daemon download failed due to: \x1b[35m${error}\x1b[0m`);
reject(error);
});
}

View file

@ -1,6 +1,6 @@
{
"name": "LBRY",
"version": "0.32.2",
"version": "0.33.0",
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
"keywords": [
"lbry"
@ -37,7 +37,7 @@
"flow-defs": "flow-typed install",
"precommit": "lint-staged",
"preinstall": "yarn cache clean lbry-redux && yarn cache clean lbryinc",
"postinstall": "electron-builder install-app-deps && node build/downloadDaemon.js"
"postinstall": "electron-builder install-app-deps && node ./build/downloadDaemon.js"
},
"dependencies": {
"electron-dl": "^1.11.0",
@ -81,6 +81,7 @@
"copy-webpack-plugin": "^4.6.0",
"country-data": "^0.0.31",
"cross-env": "^5.2.0",
"css-doodle": "^0.7.1",
"css-loader": "^2.1.0",
"cssnano": "^4.1.10",
"dat.gui": "^0.7.2",
@ -193,7 +194,7 @@
"yarn": "^1.3"
},
"lbrySettings": {
"lbrynetDaemonVersion": "0.37.1",
"lbrynetDaemonVersion": "0.37.2",
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
"lbrynetDaemonDir": "static/daemon",
"lbrynetDaemonFileName": "lbrynet"

View file

@ -16,7 +16,7 @@ export default appState => {
});
const windowConfiguration = {
backgroundColor: '#2f9176',
backgroundColor: '#270f34', // Located in src/scss/init/_vars.scss `--color-background`
minWidth: 950,
minHeight: 600,
autoHideMenuBar: true,

View file

@ -1,5 +1,6 @@
// @flow
import React, { Fragment } from 'react';
import MarkdownPreview from 'component/common/markdown-preview';
type Props = {
description: ?string,
@ -7,6 +8,15 @@ type Props = {
website: ?string,
};
const formatEmail = (email: string) => {
if (email) {
const protocolRegex = new RegExp('^mailto:', 'i');
const protocol = protocolRegex.exec(email);
return protocol ? email : `mailto:${email}`;
}
return null;
};
function ChannelContent(props: Props) {
const { description, email, website } = props;
const showAbout = description || email || website;
@ -16,17 +26,25 @@ function ChannelContent(props: Props) {
{!showAbout && <h2 className="empty">{__('Nothing here yet')}</h2>}
{showAbout && (
<Fragment>
{description && <div className="media__info-text">{description}</div>}
{description && (
<div className="media__info-text media__info-text--small">
<MarkdownPreview content={description} promptLinks />
</div>
)}
{email && (
<Fragment>
<div className="media__info-title">{__('Contact')}</div>
<div className="media__info-text">{email}</div>
<div className="media__info-text">
<MarkdownPreview content={formatEmail(email)} promptLinks />
</div>
</Fragment>
)}
{website && (
<Fragment>
<div className="media__info-title">{__('Site')}</div>
<div className="media__info-text">{website}</div>
<div className="media__info-text">
<MarkdownPreview content={website} promptLinks />
</div>
</Fragment>
)}
</Fragment>

View file

@ -0,0 +1,5 @@
/* eslint react/display-name: 0 */
import React from 'react';
export default ({ rule = '' }) => <css-doodle use="var(--rule)">{rule}</css-doodle>;

View file

@ -5,6 +5,7 @@ import Yrbl from 'component/yrbl';
import Button from 'component/button';
import { withRouter } from 'react-router';
import Native from 'native';
import { Lbry } from 'lbry-redux';
type Props = {
children: React.Node,
@ -30,20 +31,22 @@ class ErrorBoundary extends React.Component<Props, State> {
}
componentDidCatch(error: { stack: string }) {
let errorMessage = '\n';
let errorMessage = 'Uncaught error\n';
// @if TARGET='web'
errorMessage += 'lbry.tv error\n';
errorMessage += 'lbry.tv\n';
errorMessage += window.location.pathname + window.location.search;
this.log(errorMessage);
// @endif
// @if TARGET='app'
Native.getAppVersionInfo().then(({ localVersion }) => {
errorMessage += `version: ${localVersion}\n`;
errorMessage += `page: ${window.location.href.split('.html')[1]}\n`;
errorMessage += `${error.stack}`;
this.log(errorMessage);
Lbry.version().then(({ lbrynet_version: sdkVersion }) => {
errorMessage += `app version: ${localVersion}\n`;
errorMessage += `sdk version: ${sdkVersion}\n`;
errorMessage += `page: ${window.location.href.split('.html')[1]}\n`;
errorMessage += `${error.stack}`;
this.log(errorMessage);
});
});
// @endif
}

View file

@ -24,12 +24,12 @@ class ExternalLink extends React.PureComponent<Props> {
createLink() {
const { href, title, children, openModal } = this.props;
// Regex for url protocol
const protocolRegex = new RegExp('^(https?|lbry)+:', 'i');
const protocolRegex = new RegExp('^(https?|lbry|mailto)+:', 'i');
const protocol = href ? protocolRegex.exec(href) : null;
// Return plain text if no valid url
let element = <span>{children}</span>;
// Return external link if protocol is http or https
if (protocol && (protocol[0] === 'http:' || protocol[0] === 'https:')) {
if (protocol && (protocol[0] === 'http:' || protocol[0] === 'https:' || protocol[0] === 'mailto:')) {
element = (
<Button
button="link"

View file

@ -77,10 +77,12 @@ class MediaPlayer extends React.PureComponent<Props, State> {
// Temp hack to force the video to play if the metadataloaded event was never fired
// Will be removed with the new video player
// Unoptimized MP4s will fail to render.
// Note: Don't use this for non-playable files
// @if TARGET='app'
setTimeout(() => {
const { hasMetadata } = this.state;
if (!hasMetadata) {
const isPlayableType = this.playableType();
if (!hasMetadata && isPlayableType) {
this.refreshMetadata();
this.playMedia();
}
@ -88,6 +90,20 @@ class MediaPlayer extends React.PureComponent<Props, State> {
// @endif
}
componentDidUpdate(prevProps: Props) {
const { fileSource } = this.state;
const { downloadCompleted } = this.props;
// Attemp to render a non-playable file once download is completed
if (prevProps.downloadCompleted !== downloadCompleted) {
const isFileType = this.isSupportedFile();
if (isFileType && !fileSource && downloadCompleted) {
this.playMedia();
}
}
}
componentWillUnmount() {
const mediaElement = this.mediaContainer.current.children[0];
@ -139,8 +155,10 @@ class MediaPlayer extends React.PureComponent<Props, State> {
};
// Render custom viewer: FileRender
if (this.isSupportedFile() && downloadCompleted) {
this.renderFile();
if (this.isSupportedFile()) {
if (downloadCompleted) {
this.renderFile();
}
} else {
// Render default viewer: render-media (video, audio, img, iframe)
const currentMediaContainer = this.mediaContainer.current;
@ -256,9 +274,7 @@ class MediaPlayer extends React.PureComponent<Props, State> {
isRenderMediaSupported() {
// Files supported by render-media
const { contentType } = this.props;
return (
Object.values(player.mime).indexOf(contentType) !== -1 || MediaPlayer.SANDBOX_TYPES.indexOf(contentType) > -1
);
return Object.values(player.mime).indexOf(contentType) !== -1;
}
isSupportedFile() {
@ -274,33 +290,36 @@ class MediaPlayer extends React.PureComponent<Props, State> {
if (MediaPlayer.SANDBOX_TYPES.indexOf(contentType) > -1) {
const outpoint = `${claim.txid}:${claim.nout}`;
return fetch(`${MediaPlayer.SANDBOX_SET_BASE_URL}${outpoint}`)
// Fetch unpacked url
fetch(`${MediaPlayer.SANDBOX_SET_BASE_URL}${outpoint}`)
.then(res => res.text())
.then(url => {
const fileSource = { url: `${MediaPlayer.SANDBOX_CONTENT_BASE_URL}${url}` };
return this.setState({ fileSource });
this.setState({ fileSource });
})
.catch(err => {
console.error(err);
});
} else {
// File to render
const fileSource = {
fileName,
contentType,
downloadPath,
fileType: path.extname(fileName).substring(1),
// Readable stream from file
// @if TARGET='app'
stream: opts => fs.createReadStream(downloadPath, opts),
// @endif
};
// Update state
this.setState({ fileSource });
}
// File to render
const fileSource = {
fileName,
contentType,
downloadPath,
fileType: path.extname(fileName).substring(1),
// Readable stream from file
// @if TARGET='app'
stream: opts => fs.createReadStream(downloadPath, opts),
// @endif
};
// Update state
this.setState({ fileSource });
}
showLoadingScreen(isFileType: boolean, isPlayableType: boolean) {
const { mediaType, contentType } = this.props;
const { mediaType } = this.props;
const { unplayable, fileSource, hasMetadata } = this.state;
if (IS_WEB && ['audio', 'video'].indexOf(mediaType) === -1) {
@ -325,10 +344,7 @@ class MediaPlayer extends React.PureComponent<Props, State> {
// Files
const isLoadingFile = !fileSource && isFileType;
const isLbryPackage = /application\/x(-ext)?-lbry$/.test(contentType);
const isUnsupported =
(mediaType === 'application' && !isLbryPackage) ||
(!this.isRenderMediaSupported() && !isFileType && !isPlayableType);
const isUnsupported = !this.isRenderMediaSupported() && !isFileType && !isPlayableType;
// Media (audio, video)
const isUnplayable = isPlayableType && unplayable;
const isLoadingMetadata = isPlayableType && (!hasMetadata && !unplayable);
@ -341,8 +357,6 @@ class MediaPlayer extends React.PureComponent<Props, State> {
// Show unsupported error message
} else if (isUnsupported || isUnplayable) {
loader.loadingStatus = isUnsupported ? unsupportedMessage : unplayableMessage;
} else if (isLbryPackage && !isLoadingFile) {
loader.loadingStatus = null;
}
return loader;
@ -352,8 +366,7 @@ class MediaPlayer extends React.PureComponent<Props, State> {
const { mediaType, claim } = this.props;
const { fileSource } = this.state;
const isFileType = this.isSupportedFile();
const isFileReady = fileSource && isFileType;
const isFileReady = fileSource !== null && isFileType;
const isPlayableType = this.playableType();
const { isLoading, loadingStatus } = this.showLoadingScreen(isFileType, isPlayableType);

View file

@ -37,6 +37,7 @@ export default function AppRouter() {
<Scroll>
<Switch>
<Route path="/" exact component={DiscoverPage} />
<Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} />
<Route path={`/$/${PAGES.AUTH}`} exact component={AuthPage} />
<Route path={`/$/${PAGES.INVITE}`} exact component={InvitePage} />
<Route path={`/$/${PAGES.DOWNLOADED}`} exact component={FileListDownloaded} />

View file

@ -1,78 +0,0 @@
// @flow
import * as ICONS from 'constants/icons';
import React, { Fragment } from 'react';
import Icon from 'component/common/icon';
import Spinner from 'component/spinner';
import Button from 'component/button';
type Props = {
message: string,
details: ?string,
isWarning: boolean,
error: boolean,
};
class LoadScreen extends React.PureComponent<Props> {
static defaultProps = {
isWarning: false,
};
render() {
const { details, message, isWarning, error } = this.props;
return (
<div className="load-screen">
<div>
<div className="load-screen__header">
<h1 className="load-screen__title">
{__('LBRY')}
<sup>beta</sup>
</h1>
</div>
{error ? (
<Fragment>
<h3>{__('Uh oh. Sean must have messed something up. Try refreshing to fix it.')}</h3>
<div className="load-screen__actions">
<Button
label="Refresh"
button="link"
className="load-screen__button"
onClick={() => window.location.reload()}
/>
</div>
<div className="load-screen__help">
<p>{__('If you still have issues, your anti-virus software or firewall may be preventing startup.')}</p>
<p>
{__('Reach out to hello@lbry.com for help, or check out')}{' '}
<Button
button="link"
className="load-screen__button"
href="https://lbry.com/faq/startup-troubleshooting"
label="this link"
/>
.
</p>
</div>
</Fragment>
) : (
<Fragment>
{isWarning ? (
<span className="load-screen__message">
<Icon size={20} icon={ICONS.ALERT} />
{` ${message}`}
</span>
) : (
<div className="load-screen__message">{message}</div>
)}
{details && <div className="load-screen__details">{details}</div>}
<Spinner type="splash" />
</Fragment>
)}
</div>
</div>
);
}
}
export default LoadScreen;

View file

@ -1,14 +1,15 @@
// @flow
import * as React from 'react';
import * as MODALS from 'constants/modal_types';
import React from 'react';
import { Lbry } from 'lbry-redux';
import Button from 'component/button';
import ModalWalletUnlock from 'modal/modalWalletUnlock';
import ModalIncompatibleDaemon from 'modal/modalIncompatibleDaemon';
import ModalUpgrade from 'modal/modalUpgrade';
import ModalDownloading from 'modal/modalDownloading';
import LoadScreen from './internal/load-screen';
import 'css-doodle';
const ONE_MINUTE = 60 * 1000;
const FORTY_FIVE_SECONDS = 45 * 1000;
type Props = {
checkDaemonVersion: () => Promise<any>,
@ -87,7 +88,7 @@ export default class SplashScreen extends React.PureComponent<Props, State> {
// If nothing changes after 1 minute, show the error message.
this.timeout = setTimeout(() => {
this.setState({ error: true });
}, ONE_MINUTE);
}, FORTY_FIVE_SECONDS);
}
updateStatus() {
@ -206,16 +207,66 @@ export default class SplashScreen extends React.PureComponent<Props, State> {
}
render() {
const { message, details, error } = this.state;
const { error } = this.state;
return (
<React.Fragment>
<LoadScreen message={message} details={details} error={error} />
<div className="splash">
<css-doodle>
{`
--color: @p(var(--lbry-teal-1), var(--lbry-orange-1), var(--lbry-cyan-3), var(--lbry-pink-5));
:doodle {
@grid: 30x1 / 18vmin;
--deg: @p(-180deg, 180deg);
}
:container {
perspective: 30vmin;
}
@place-cell: center;
@size: 100%;
box-shadow: @m2(0 0 50px var(--color));
will-change: transform, opacity;
animation: scale-up 12s linear infinite;
animation-delay: calc(-12s / @size() * @i());
@keyframes scale-up {
0%, 95.01%, 100% {
transform: translateZ(0) rotate(0);
opacity: 0;
}
10% {
opacity: 1;
}
95% {
transform:
translateZ(35vmin) rotateZ(@var(--deg));
}
}
)
`}
</css-doodle>
<h1 className="splash__title">LBRY</h1>
{error && (
<div className="splash__error card card--section">
<h3>{__('Uh oh. The flux in our Retro Encabulator must be out of whack. Try refreshing to fix it.')}</h3>
<div className="card__actions--top-space card__actions--center">
<Button button="primary" label={__('Refresh')} onClick={() => window.location.reload()} />
</div>
<div className="help">
<p>{__('If you still have issues, your anti-virus software or firewall may be preventing startup.')}</p>
<p>
{__('Reach out to hello@lbry.com for help, or check out')}{' '}
<Button button="link" href="https://lbry.com/faq/startup-troubleshooting" label="this link" />.
</p>
</div>
</div>
)}
{/* Temp hack: don't show any modals on splash screen daemon is running;
daemon doesn't let you quit during startup, so the "Quit" buttons
in the modals won't work. */}
in the modals won't work. */}
{this.renderModals()}
</React.Fragment>
</div>
);
}
}

View file

@ -37,13 +37,17 @@ class TransactionListItem extends React.PureComponent<Props> {
this.props.revokeClaim(txid, nout);
}
capitalize = (string: string) => string.charAt(0).toUpperCase() + string.slice(1);
capitalize = (string: ?string) => string && string.charAt(0).toUpperCase() + string.slice(1);
render() {
const { reward, transaction, isRevokeable } = this.props;
const { amount, claim_id: claimId, claim_name: name, date, fee, txid, type } = transaction;
// Ensure the claim name is valid
const { claimName } = parseURI(name);
// Ensure the claim name exists and is valid
let claimName = name;
if (claimName) {
({ claimName } = parseURI(name));
}
const dateFormat = {
month: 'short',

View file

@ -10,8 +10,14 @@ type Props = {
},
};
let workerPath = 'webworkers/worker-bundle.js';
if (process.env.NODE_ENV !== 'production') {
// Don't add a leading slash in production because electron treats it as an absolute path
workerPath = `/${workerPath}`;
}
const opts = {
workerPath: '/webworkers/worker-bundle.js',
workerPath,
allowFullScreen: false,
autoHideControls: true,
};

View file

@ -28,7 +28,7 @@ class AudioVideoViewer extends React.PureComponent<Props> {
// Will need to be changed to include time to start
analytics.apiLogView(`${name}#${claimId}`, `${txid}:${nout}`, claimId);
const path = `https://api.piratebay.com/content/claims/${claim.name}/${claim.claim_id}/stream.mp4`;
const path = `https://api.lbry.tv/content/claims/${claim.name}/${claim.claim_id}/stream.mp4`;
const sources = [
{
src: path,

View file

@ -44,7 +44,7 @@ if (process.env.SEARCH_API_URL) {
}
// @if TARGET='web'
const SDK_API_URL = process.env.SDK_API_URL || 'https://api.piratebay.com/api/proxy';
const SDK_API_URL = process.env.SDK_API_URL || 'https://api.lbry.tv/api/proxy';
Lbry.setDaemonConnectionString(SDK_API_URL);
// @endif

View file

@ -24,7 +24,6 @@
@import 'component/header';
@import 'component/icon';
@import 'component/item-list';
@import 'component/load-screen';
@import 'component/main';
@import 'component/markdown-editor';
@import 'component/markdown-preview';
@ -38,6 +37,7 @@
@import 'component/search';
@import 'component/snack-bar';
@import 'component/spinner';
@import 'component/splash';
@import 'component/subscriptions';
@import 'component/syntax-highlighter';
@import 'component/table';

View file

@ -1,86 +0,0 @@
.load-screen {
min-width: 100vw;
min-height: 100vh;
align-items: center;
background-image: linear-gradient(to right, $lbry-teal-5, $lbry-cyan-5 100%);
background-size: cover;
color: $lbry-white;
cursor: default;
display: flex;
justify-content: center;
text-align: center;
.spinner {
margin-right: auto;
margin-left: auto;
}
}
.load-screen__button {
transition: none;
color: $lbry-white;
border-bottom: 1px solid $lbry-white;
&:hover {
border-bottom: 1px solid $lbry-blue-1;
color: $lbry-blue-1;
}
}
.load-screen__details {
font-weight: 600;
line-height: 1;
max-width: 400px;
}
.load-screen__header {
display: flex;
justify-content: center;
margin-bottom: var(--spacing-vertical-medium);
}
.load-screen__help {
font-size: 1.25rem;
padding-top: var(--spacing-vertical-large);
}
.load-screen__message {
font-size: 16px;
font-weight: 800;
line-height: 1;
margin-bottom: var(--spacing-vertical-medium);
}
.load-screen__title {
font-size: 60px;
font-weight: 700;
line-height: 1;
position: relative;
sup {
margin-left: -var(--spacing-vertical-tiny);
padding: var(--spacing-vertical-miniscule) var(--spacing-vertical-small);
background-color: rgba($lbry-white, 0.3);
border-radius: 0.2rem;
color: $lbry-white;
font-size: 0.6rem;
font-weight: 400;
letter-spacing: 0.1rem;
line-height: 1;
position: absolute;
text-transform: uppercase;
top: 2.15rem;
}
}
.load-screen--help {
font-size: 14px;
padding-top: $spacing-vertical;
}
.load-screen__actions {
font-size: 1.2em;
margin-top: var(--spacing-vertical-medium);
}

View file

@ -260,12 +260,16 @@
.media__info-text {
font-size: 1.15rem;
word-break: break-all;
word-break: break-word;
&:not(:last-of-type) {
margin-bottom: var(--spacing-vertical-large);
}
&.media__info-text--small {
max-width: 50rem;
}
&.media__info-text--center {
text-align: center;
}

View file

@ -90,8 +90,7 @@
.error-modal__content {
display: flex;
padding: 0 var(--spacing-vertical-medium) var(--spacing-vertical-medium)
var(--spacing-vertical-medium);
padding: 0 var(--spacing-vertical-medium) var(--spacing-vertical-medium) var(--spacing-vertical-medium);
}
.error-modal__warning-symbol {

View file

@ -0,0 +1,46 @@
.splash {
width: 100vw;
height: 100vh;
align-items: center;
background-color: var(--color-background);
display: flex;
justify-content: center;
overflow: hidden;
}
// .splash__actions {
// margin-top: var(--spacing-vertical-medium);
// align-items: center;
// }
.splash__button {
border-bottom: 1px solid $lbry-white;
color: $lbry-white;
transition: none;
&:hover {
border-bottom: 1px solid $lbry-blue-1;
color: $lbry-blue-1;
}
}
.splash__details {
font-weight: 600;
line-height: 1;
max-width: 400px;
}
.splash__title {
position: absolute;
font-size: 40px;
line-height: 1;
font-weight: 700;
color: $lbry-white;
}
.splash__error {
position: absolute;
margin-top: 25rem;
box-shadow: var(--card-box-shadow) $lbry-gray-5;
}

View file

@ -9,7 +9,6 @@ $large-breakpoint: 1921px;
// Width & spacing
--side-nav-width: 180px;
--font-size-subtext-multiple: 0.92;
--spacing-vertical-miniscule: calc(2rem / 5);
--spacing-vertical-tiny: calc(2rem / 4);
--spacing-vertical-small: calc(2rem / 3);
@ -17,15 +16,15 @@ $large-breakpoint: 1921px;
--spacing-vertical-large: 2rem;
--spacing-vertical-xlarge: 3rem;
--spacing-main-padding: var(--spacing-vertical-xlarge);
--file-page-max-width: 1787px;
--file-max-height: 788px;
--file-max-width: 1400px;
--video-aspect-ratio: 56.25%; // 9 x 16
--channel-thumbnail-size: 10rem;
// Color
--color-background: #270f34;
// Text
--text-max-width: 660px;
--text-link-padding: 4px;

View file

@ -3001,6 +3001,11 @@ css-declaration-sorter@^4.0.1:
postcss "^7.0.1"
timsort "^0.3.0"
css-doodle@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/css-doodle/-/css-doodle-0.7.1.tgz#477466310df6554ec5182349745194ba37415670"
integrity sha512-TRs+7YLgP/yc0EdrL5Us2GRHbhIgf4GsQWAt5WdRxr5pwwpWl8Q9jZle431Z/5u5C29jiEoItjIaKE84xFh+kA==
css-hot-loader@^1.4.3:
version "1.4.4"
resolved "https://registry.yarnpkg.com/css-hot-loader/-/css-hot-loader-1.4.4.tgz#ae784932cd8b7d092f7f15702af08b3ec9436052"