(Almost the) rest of the redesign (#880)

* Load demo img

* two-up styles

* Back button

* Button size tweak

* Move back btn

* Move options and back button into a single grid

* Simpler max height

* Responsive grid

* Feed index into options

* Option heading themes

* More option styles

* Changing checkbox position

* Theme range input & use transforms

* Range input underline theme

* Checkbox color

* Add toggle

* Reorder

* Arrow revealer

* Round two-up thumb

* Don't bundle CSS urls starting #

* Results in progress

* Fix Safari bugs

* Download blobs

* Loading spinner

* Hook up download button

* Different style for original image

* Mobile design for results

* Remove demo auto-loader

* Remove redundant colors

* Sticky headings
This commit is contained in:
Jake Archibald
2020-12-09 11:47:23 +00:00
committed by GitHub
parent 12889d9d50
commit fec826b106
36 changed files with 903 additions and 497 deletions

View File

@@ -1,43 +0,0 @@
import { h, Component } from 'preact';
import prettyBytes from 'pretty-bytes';
import * as style from './style.css';
interface Props {
blob: Blob;
compareTo?: Blob;
}
interface State {}
export default class FileSize extends Component<Props, State> {
render({ blob, compareTo }: Props) {
let comparison: preact.JSX.Element | undefined;
if (compareTo) {
const delta = blob.size / compareTo.size;
if (delta > 1) {
const percent = Math.round((delta - 1) * 100) + '%';
comparison = (
<span class={`${style.sizeDelta} ${style.sizeIncrease}`}>
{percent === '0%' ? 'slightly' : percent} bigger
</span>
);
} else if (delta < 1) {
const percent = Math.round((1 - delta) * 100) + '%';
comparison = (
<span class={`${style.sizeDelta} ${style.sizeDecrease}`}>
{percent === '0%' ? 'slightly' : percent} smaller
</span>
);
} else {
comparison = <span class={style.sizeDelta}>no change</span>;
}
}
return (
<span>
{prettyBytes(blob.size)} {comparison}
</span>
);
}
}

View File

@@ -1,37 +1,25 @@
import { h, Component, ComponentChildren, ComponentChild } from 'preact';
import { h, Component, Fragment } from 'preact';
import * as style from './style.css';
import 'add-css:./style.css';
import FileSize from './FileSize';
import {
DownloadIcon,
CopyAcrossIcon,
CopyAcrossIconProps,
} from 'client/lazy-app/icons';
import 'shared/custom-els/loading-spinner';
import { SourceImage } from '../';
import prettyBytes from './pretty-bytes';
import { Arrow, DownloadIcon } from 'client/lazy-app/icons';
interface Props {
loading: boolean;
source?: SourceImage;
imageFile?: File;
downloadUrl?: string;
children: ComponentChildren;
copyDirection: CopyAcrossIconProps['copyDirection'];
buttonPosition: keyof typeof buttonPositionClass;
onCopyToOtherClick(): void;
flipSide: boolean;
typeLabel: string;
}
interface State {
showLoadingState: boolean;
}
const buttonPositionClass = {
'stack-right': style.stackRight,
'download-right': style.downloadRight,
'download-left': style.downloadLeft,
};
const loadingReactionDelay = 500;
export default class Results extends Component<Props, State> {
@@ -56,11 +44,6 @@ export default class Results extends Component<Props, State> {
}
}
private onCopyToOtherClick = (event: Event) => {
event.preventDefault();
this.props.onCopyToOtherClick();
};
private onDownload = () => {
// GA cant do floats. So we round to ints. We're deliberately rounding to nearest kilobyte to
// avoid cases where exact image sizes leak something interesting about the user.
@@ -76,59 +59,83 @@ export default class Results extends Component<Props, State> {
};
render(
{
source,
imageFile,
downloadUrl,
children,
copyDirection,
buttonPosition,
}: Props,
{ source, imageFile, downloadUrl, flipSide, typeLabel }: Props,
{ showLoadingState }: State,
) {
const prettySize = imageFile && prettyBytes(imageFile.size);
const isOriginal = !source || !imageFile || source.file === imageFile;
let diff;
let percent;
if (source && imageFile) {
diff = imageFile.size / source.file.size;
const absolutePercent = Math.round(Math.abs(diff) * 100);
percent = diff > 1 ? absolutePercent - 100 : 100 - absolutePercent;
}
return (
<div class={`${style.results} ${buttonPositionClass[buttonPosition]}`}>
<div class={style.resultData}>
{children ? <div class={style.resultTitle}>{children}</div> : null}
{!imageFile || showLoadingState ? (
'Working…'
) : (
<FileSize
blob={imageFile}
compareTo={
source && imageFile !== source.file ? source.file : undefined
}
/>
)}
<div
class={
(flipSide ? style.resultsRight : style.resultsLeft) +
' ' +
(isOriginal ? style.isOriginal : '')
}
>
<div class={style.expandArrow}>
<Arrow />
</div>
<button
class={style.copyToOther}
title="Copy settings to other side"
onClick={this.onCopyToOtherClick}
<div class={style.bubble}>
<div class={style.bubbleInner}>
<div class={style.sizeInfo}>
<div class={style.fileSize}>
{prettySize ? (
<Fragment>
{prettySize.value}{' '}
<span class={style.unit}>{prettySize.unit}</span>
<span class={style.typeLabel}> {typeLabel}</span>
</Fragment>
) : (
'…'
)}
</div>
</div>
<div class={style.percentInfo}>
<svg
viewBox="0 0 1 2"
class={style.bigArrow}
preserveAspectRatio="none"
>
<path d="M1 0v2L0 1z" />
</svg>
<div class={style.percentOutput}>
{diff && diff !== 1 && (
<span class={style.sizeDirection}>
{diff < 1 ? '↓' : '↑'}
</span>
)}
<span class={style.sizeValue}>{percent || 0}</span>
<span class={style.percentChar}>%</span>
</div>
</div>
</div>
</div>
<a
class={showLoadingState ? style.downloadDisable : style.download}
href={downloadUrl}
download={imageFile ? imageFile.name : ''}
title="Download"
onClick={this.onDownload}
>
<CopyAcrossIcon
class={style.copyIcon}
copyDirection={copyDirection}
/>
</button>
<div class={style.download}>
{downloadUrl && imageFile && (
<a
class={`${style.downloadLink} ${
showLoadingState ? style.downloadLinkDisable : ''
}`}
href={downloadUrl}
download={imageFile.name}
title="Download"
onClick={this.onDownload}
>
<DownloadIcon class={style.downloadIcon} />
</a>
)}
{showLoadingState && <loading-spinner class={style.spinner} />}
</div>
<svg class={style.downloadBlobs} viewBox="0 0 89.6 86.9">
<title>Download</title>
<path d="M27.3 72c-8-4-15.6-12.3-16.9-21-1.2-8.7 4-17.8 10.5-26s14.4-15.6 24-16 21.2 6 28.6 16.5c7.4 10.5 10.8 25 6.6 34S64.1 71.8 54 73.6c-10.2 2-18.7 2.3-26.7-1.6z" />
<path d="M19.8 24.8c4.3-7.8 13-15 21.8-15.7 8.7-.8 17.5 4.8 25.4 11.8 7.8 6.9 14.8 15.2 14.7 24.9s-7.1 20.7-18 27.6c-10.8 6.8-25.5 9.5-34.2 4.8S18.1 61.6 16.7 51.4c-1.3-10.3-1.3-18.8 3-26.6z" />
</svg>
<div class={style.downloadIcon}>
<DownloadIcon />
</div>
{showLoadingState && <loading-spinner />}
</a>
</div>
);
}

View File

@@ -0,0 +1,27 @@
// Based on https://www.npmjs.com/package/pretty-bytes
// Modified so the units are returned separately.
const UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
interface PrettyBytesResult {
value: string;
unit: string;
}
export default function prettyBytes(number: number): PrettyBytesResult {
const isNegative = number < 0;
const prefix = isNegative ? '-' : '';
if (isNegative) number = -number;
if (number < 1) return { value: prefix + number, unit: UNITS[0] };
const exponent = Math.min(
Math.floor(Math.log10(number) / 3),
UNITS.length - 1,
);
return {
unit: UNITS[exponent],
value: prefix + (number / Math.pow(1000, exponent)).toPrecision(3),
};
}

View File

@@ -1,3 +1,12 @@
@font-face {
font-family: 'Roboto Mono Numbers';
font-style: normal;
font-weight: 700;
/* Just 0132456789. https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@700&text=0123456789 */
src: url('data:font/woff;base64,d09GRgABAAAAAAkEAA0AAAAACygAAQABAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABMAAAADYAAAA2kxWCFk9TLzIAAAFoAAAAYAAAAGCY9cGQU1RBVAAAAcgAAABEAAAAROXczCxjbWFwAAACDAAAADwAAAA8AFsAbWdhc3AAAAJIAAAACAAAAAgAAAAQZ2x5ZgAAAlAAAASiAAAF7GtBYvxoZWFkAAAG9AAAADYAAAA2ATacDmhoZWEAAAcsAAAAJAAAACQKsQEqaG10eAAAB1AAAAAaAAAAGgb1AeRsb2NhAAAHbAAAABoAAAAaCBgG1W1heHAAAAeIAAAAIAAAACAAKwE6bmFtZQAAB6gAAAE7AAACbDvbXDhwb3N0AAAI5AAAACAAAAAg/20AZQABAAAACgAyADQABERGTFQAGmN5cmwAJGdyZWsAJGxhdG4AJAAEAAAAAP//AAAAAAAAAAAAAAAAAAQEzQK8AAUAAAWaBTMAAAEfBZoFMwAAA9EAZgIAAAAAAAAJAAAAAAAA4AAC/xAAIFsAAAAgAAAAAEdPT0cAIAAgADkIYv3VAAAIYgIrIAABn08BAAAEOgWwAAAAIAABAAEAAQAIAAIAAAAUAAIAAAAkAAJ3Z2h0AQAAAGl0YWwBCwABAAQAEAABAAAAAAEQArwAAAADAAEAAgERAAAAAAABAAAAAAACAAAAAwAAABQAAwABAAAAFAAEACgAAAAGAAQAAQACACAAOf//AAAAIAAw////4f/SAAEAAAAAAAAAAQAB//8AD3icjZRLbBtVFIbv9SsihTROMh7P056M37FrJ54ZO/Y4sT12/IidOCl9JU3bxGneSaPS0CLBolC6i5AAKYukCEQrqFBaQ0ok1AqEVCQ2bGCRAgKEKpAogQVigdTYYSZeYBEhMaure4/u/P93/nOBGizv/qqJaT8DGHCBMAAxPWez22xsq65Op0P0LQbUYPB3CAFBgBzH85yy8ncou6i+RalhW5V6O2xpQTSxeKSrtDB/O9IVl7oipfmFUiQSK49juDHldUkobVJhmDHt9WeNCKqCV1QueHJ5K5POZtOZreXK9eWtdCabzaS3oNZk9r018KyVZQmSXRqsRAYu2iysw9k6EYdmysQACNYBUAvaEtABMKpntYhVrxYOlq/CRW3ppz+uPH4byDU9AGiS2vuyMzDKM1BQxPO1/pgaP8ienTqIaJLly7ApdcHl9IidoffOXfhESmQhSlPkkWBbCsdIBDXmAhXnD7A183I0+lL69LVgsKs3FlsfDZ800SYSJ3LtTI/DOSZWdB8rOs7sbmvSso6czBep/VsVHs/UYK7qs/8frSy8sdI5zJhbKZI6KvgKFG2uPDqcTL4/Mnc3kegjKepEhCsQBJmKRu/Mja9HoxloMJNExuXPY8pHHBPVgw9wgng67M3hFE3hWMo1s+bn/J0B4c2J0JS7LWHAkk7nsHdilef4EMe/esIRRQ1GEsNTbe5enGYAUCm50YzJvagHDUqCGIgyekbvl5EH9OqPKj+X1w6oRqDh5s4jKBIqSv3aTuhW5T4Uf4S/+coPFUJLMqEe+QYfAIdRYZ9RGRNj+DcjhUszwzOr3zdaUFR0RoZomkKNWH9weKm+8rv6SCJx+9Tzm6IYEoOdN6az502sStp5oPoi0EgdONBgY9nRUHjCanVNnT57TRCCfZJUGntms7tbsjXB6Lbsa1pWldXeA3YQlX2xrZo6nQpB9jWr2qC6qmw/3N9ffu9Ifc76YWV7ZORUKh67t7R4p7s74ee41Sn/catNRJ9IiuGbC42VbW6AJGmaJAvtvkGaZhqcGNWjvffc7Fzl6/XZF77K54/lJWlzpriRkPqNzS3t2PDrPB+qoE6LdTwcLlosTofDfqkwSHc0I6jCNil3J1GdlBjPIAxkNIkdqPq2/Dm0apHVh4/98iiBedlrl5xRL0iB03JlbfT2HKO6f9b7o6qu9VuT1P/a15iSka53i8V3xEiCIrCMhz8im+2NxzemJj+Ix3oFHx63OiXMzP5lIsjcIW+OoMw0QeR9vjxJ0BSGonGX/KYYjShqiLe5JCOKazzFlb1HilspCmNOexTFUm7PrDi5xvGK2rXJvisdpKcJ6WTc0+VSRz9JUgRODHIdBUpBThVUeXcGxykKxzMed0YeEDnnaSXho7t/arwyHQeIAXCWlYfYEhCaeH4fpSqZALIHq3mfbaR6AHPNyKfQMDR0VOqObp5f3JBDFxD4N6baBxkTh9RHhOD1V4QCTuAkjufbPX0UxTxpx4nYd79cnJ2BlltnXvymv//4QE/P3ZnJjXg8pz/4lAXRFS67D/nglx6bbSIYnHTYvQ6H41KhaOKbWwzgbxq6UxkAAAABAAAAAwAA+7NEP18PPPUACwgAAAAAAMTwES4AAAAA2tg/q/wF/dUGRwhiAAEACQACAAAAAAAAAAEAAAhi/dUAAATN/AX+hgZHAAEAAAAAAAAAAAAAAAAAAAABBM0AAAAAAI0ArQBGAGAAOwB1AGkARQBtAGEAAAAAAAAAAABcAG4AsgEiAUMBjgHxAgQCkwL2AAAAAQAAAAwAsQAWAIcABQABAAAAAAAAAAAAAAAAAAMAAXicfZG9TgJBFIW/ESQajdHGwsJsZbRgwb9GG39iCImiUaKdyYq4YFjWwBLji/ggxtoHoPSJPDs7qzSYmztz5s6cc+bOAIu8U8AU54FPZYYNq1pleIY5xg4X2OPb4SKeKTk8y5rZcLjEujlyeImmuc+wkZf5cHhB+Mvh5T99s6L6mFNiXnhjQJeQDgkeO1TZZl+oqUpb87VOPSgTpceFxr5FV+LFPOtMyzKPGWnuqDZgqPWmVUzkMOSAiiKUT3piJD1frJjIVmNFSE9KT1Y9EaNi1XPfyLluTbnNibLHI7vSrdo4pMaloiY0yckZ5V/OtP7y/VvdK+2oa3e8CY//dfPus95fbfgEqgTqPX1b375VqN2e1Fuq9OXTtt2fU9f/nNHgRmNZ/5K63mk3/6u6MnDMhlWK0vUPUDBdTwAAAwAAAAAAAP9qAGQAAAABAAAAAAAAAAAAAAAAAAAAAA==')
format('woff');
}
@keyframes action-enter {
from {
transform: rotate(-90deg);
@@ -15,117 +24,336 @@
}
.results {
--download-overflow-size: 9px;
background: rgba(0, 0, 0, 0.67);
border-radius: 5px;
display: grid;
grid-template-columns: [text] 1fr [copy-button] auto [download-button] auto;
background: rgba(0, 0, 0, 0.9);
font-size: 1rem;
@media (min-width: 400px) {
font-size: 1.2rem;
}
grid-template-columns: max-content [bubble] 1fr [download] max-content;
@media (min-width: 600px) {
font-size: 1.4rem;
}
&:focus {
outline: none;
--download-overflow-size: 30px;
background: none;
border-radius: none;
grid-template-columns: [download] auto [bubble] 1fr;
align-items: center;
margin-bottom: calc(var(--download-overflow-size) / 2);
}
}
.result-data {
.expand-arrow {
fill: var(--white);
transform: rotate(180deg);
margin: 0 1rem;
align-self: center;
@media (min-width: 600px) {
display: none;
}
:focus & {
fill: var(--main-theme-color);
}
[content-expanded] & {
transform: none;
}
svg {
display: block;
--size: 15px;
width: var(--size);
height: var(--size);
}
}
.file-size {
}
.bubble {
align-self: center;
@media (min-width: 600px) {
position: relative;
width: max-content;
grid-row: 1;
grid-column: bubble;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-image-source: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='186.5' height='280.3' viewBox='0 0 186.5 280.3'%3E%3Cpath fill='rgba(30,31,29,0.69)' d='M181.5 0H16.4a5 5 0 00-5 5v134L0 146.5h11.4v128.8a5 5 0 005 5h165.1a5 5 0 005-5V5a5 5 0 00-5-5z'/%3E%3Cpath fill='rgba(0,0,0,0.23)' d='M16.4 1a4 4 0 00-4 4v134.5l-.5.3-8.6 5.7h9v129.8a4 4 0 004 4h165.2a4 4 0 004-4V5a4 4 0 00-4-4H16.4m0-1h165.1a5 5 0 015 5v270.3a5 5 0 01-5 5H16.4a5 5 0 01-5-5V146.5H0l11.4-7.5V5a5 5 0 015-5z'/%3E%3C/svg%3E");
border-image-slice: 12 12 12 17 fill;
border-image-width: 12px 12px 12px 17px;
border-image-repeat: repeat;
}
}
}
.bubble-inner {
display: grid;
grid-template-columns: [size-info] 1fr [percent-info] auto;
@media (min-width: 600px) {
position: relative;
--main-padding: 1px;
--speech-padding: 2.1rem;
padding: var(--main-padding) var(--main-padding) var(--main-padding)
var(--speech-padding);
gap: 0.9rem;
}
}
.unit {
color: var(--main-theme-color);
}
.type-label {
@media (min-width: 600px) {
display: none;
}
}
.size-info {
background: var(--dark-gray);
border-radius: 19px;
align-self: center;
justify-self: start;
grid-column: size-info;
grid-row: 1;
grid-column: text;
display: flex;
align-items: center;
padding: 0 10px;
white-space: nowrap;
overflow: hidden;
}
justify-self: start;
padding: 0.6rem 1.2rem;
margin: 0.4rem 0;
.download-right {
grid-template-columns: [copy-button] auto [text] 1fr [download-button] auto;
}
.download-left {
grid-template-columns: [download-button] auto [text] 1fr [copy-button] auto;
}
.stack-right {
& .result-data {
padding: 0 15px;
@media (min-width: 600px) {
border-radius: none;
background: none;
padding: 0;
margin: 0;
}
}
.result-title {
display: flex;
align-items: center;
margin-right: 0.4em;
.percent-info {
align-self: center;
margin-left: 1rem;
margin-right: 0.3rem;
@media (min-width: 600px) {
margin: 0;
display: grid;
--arrow-width: 16px;
grid-template-columns: [arrow] var(--arrow-width) [data] auto;
grid-column: percent-info;
grid-row: 1;
--shadow-direction: -1px;
filter: drop-shadow(var(--shadow-direction) 0 0 rgba(0, 0, 0, 0.67));
}
}
.size-delta {
font-size: 0.8em;
font-style: italic;
.big-arrow {
display: none;
@media (min-width: 600px) {
display: block;
width: 100%;
fill: var(--main-theme-color);
grid-column: arrow;
grid-row: 1;
align-self: stretch;
}
}
.percent-output {
grid-column: data;
grid-row: 1;
display: grid;
grid-template-columns: auto auto auto;
line-height: 1;
@media (min-width: 600px) {
background: var(--main-theme-color);
--radius: 4px;
border-radius: 0 var(--radius) var(--radius) 0;
--padding-arrow-side: 0.6rem;
--padding-other-side: 1.1rem;
padding: 0.7rem var(--padding-other-side);
padding-left: var(--padding-arrow-side);
}
}
.size-direction {
font-weight: 700;
align-self: center;
font-family: sans-serif;
opacity: 0.76;
text-shadow: 0 2px rgba(0, 0, 0, 0.3);
font-size: 1.5rem;
position: relative;
top: -1px;
margin-left: 0.3em;
top: 3px;
}
.size-increase {
color: #e35050;
.size-value {
font-family: 'Roboto Mono Numbers';
font-size: 2.6rem;
text-shadow: 0 2px rgba(0, 0, 0, 0.3);
}
.size-decrease {
color: #50e3c2;
.percent-char {
align-self: start;
position: relative;
top: 4px;
opacity: 0.76;
margin-left: 0.2rem;
}
.download {
--size: 59px;
width: calc(var(--size) + var(--download-overflow-size));
height: calc(var(--size) + var(--download-overflow-size));
position: relative;
grid-row: 1;
grid-column: download-button;
background: #34b9eb;
--size: 38px;
width: var(--size);
height: var(--size);
grid-column: download;
margin: calc(var(--download-overflow-size) / -2) 0;
margin-right: calc(var(--download-overflow-size) / -3);
display: grid;
align-items: center;
justify-items: center;
align-self: center;
@media (min-width: 600px) {
--size: 63px;
}
loading-spinner {
grid-area: 1 / 1;
position: relative;
--color: var(--white);
--size: 21px;
top: 0px;
left: 1px;
@media (min-width: 600px) {
top: -1px;
left: 2px;
--size: 28px;
}
}
}
.download-link {
animation: action-enter 0.2s;
grid-area: 1/1;
.download-blobs {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
path {
fill: var(--hot-theme-color);
opacity: 0.7;
}
}
.download-link-disable {
pointer-events: none;
opacity: 0;
transform: rotate(90deg);
animation: action-leave 0.2s;
.download-icon {
grid-area: 1 / 1;
svg {
--size: 19px;
width: var(--size);
height: var(--size);
fill: var(--white);
position: relative;
top: 3px;
left: 1px;
animation: action-enter 0.2s;
@media (min-width: 600px) {
--size: 27px;
top: 2px;
left: 2px;
}
}
}
.download-icon,
.copy-icon {
color: #fff;
display: block;
--size: 24px;
width: var(--size);
height: var(--size);
padding: 7px;
filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.7));
}
.spinner {
--color: #fff;
--delay: 0;
--size: 22px;
grid-area: 1/1;
}
.copy-to-other {
grid-row: 1;
grid-column: copy-button;
composes: unbutton from global;
.download-disable {
composes: download;
background: #656565;
pointer-events: none;
.download-icon svg {
opacity: 0;
transform: rotate(90deg);
animation: action-leave 0.2s;
}
}
.results-left {
composes: results;
}
.results-right {
composes: results;
@media (min-width: 600px) {
grid-template-columns: [bubble] 1fr [download] auto;
}
.bubble {
@media (min-width: 600px) {
justify-self: end;
&::before {
transform: scaleX(-1);
}
}
}
.download {
margin-left: calc(var(--download-overflow-size) / -3);
margin-right: 0;
}
.bubble-inner {
@media (min-width: 600px) {
padding: var(--main-padding) var(--speech-padding) var(--main-padding)
var(--main-padding);
grid-template-columns: [percent-info] auto [size-info] 1fr;
}
}
.percent-info {
@media (min-width: 600px) {
grid-template-columns: [data] auto [arrow] var(--arrow-width);
--shadow-direction: 1px;
}
}
.percent-output {
@media (min-width: 600px) {
border-radius: var(--radius) 0 0 var(--radius);
padding-left: var(--padding-other-side);
padding-right: var(--padding-arrow-side);
}
}
.big-arrow {
transform: scaleX(-1);
}
}
.is-original {
.big-arrow {
fill: transparent;
}
.percent-output {
background: none;
}
.download-blobs path {
fill: var(--black);
}
.unit {
color: var(--white);
opacity: 0.76;
}
}