Styling intro screen and adding demo images.
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
BIN
src/assets/icon-large.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
src/assets/icon-small.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 27 KiB |
@@ -43,7 +43,7 @@ import Intro from '../intro';
|
||||
type Orientation = 'horizontal' | 'vertical';
|
||||
|
||||
export interface SourceImage {
|
||||
file: File;
|
||||
file: File | Fileish;
|
||||
data: ImageData;
|
||||
vectorImage?: HTMLImageElement;
|
||||
}
|
||||
@@ -259,7 +259,7 @@ export default class App extends Component<Props, State> {
|
||||
}
|
||||
|
||||
@bind
|
||||
async updateFile(file: File) {
|
||||
async updateFile(file: File | Fileish) {
|
||||
this.setState({ loading: true });
|
||||
try {
|
||||
let data: ImageData;
|
||||
@@ -268,7 +268,7 @@ export default class App extends Component<Props, State> {
|
||||
// Special-case SVG. We need to avoid createImageBitmap because of
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=606319.
|
||||
// Also, we cache the HTMLImageElement so we can perform vector resizing later.
|
||||
if (file.type === 'image/svg+xml') {
|
||||
if (file.type.startsWith('image/svg+xml')) {
|
||||
vectorImage = await processSvg(file);
|
||||
data = drawableToImageData(vectorImage);
|
||||
} else {
|
||||
@@ -368,6 +368,7 @@ export default class App extends Component<Props, State> {
|
||||
this.setState({ images });
|
||||
}
|
||||
|
||||
@bind
|
||||
showError (error: string) {
|
||||
if (!this.snackbar) throw Error('Snackbar missing');
|
||||
this.snackbar.showSnackbar({ message: error });
|
||||
@@ -410,7 +411,7 @@ export default class App extends Component<Props, State> {
|
||||
))}
|
||||
</div>
|
||||
:
|
||||
<Intro onFile={this.updateFile} />
|
||||
<Intro onFile={this.updateFile} onError={this.showError} />
|
||||
}
|
||||
{anyLoading && <span style={{ position: 'fixed', top: 0, left: 0 }}>Loading...</span>}
|
||||
<snack-bar ref={linkRef(this, 'snackbar')} />
|
||||
|
||||
@@ -10,7 +10,6 @@ Note: These styles are temporary. They will be replaced before going live.
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
contain: strict;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
:global {
|
||||
@@ -59,6 +58,7 @@ Note: These styles are temporary. They will be replaced before going live.
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.horizontal {
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -14,7 +14,6 @@ Note: These styles are temporary. They will be replaced before going live.
|
||||
|
||||
.output {
|
||||
@extend %fill;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none"><path fill="rgba(0,0,0,0.05)" d="M0 0h10v10H0zM10 10h10v10H10z"/></svg>') center repeat;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
|
||||
62
src/components/custom-els/LoadingSpinner/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as styles from './styles.css';
|
||||
|
||||
/**
|
||||
* A simple spinner. This custom element has no JS API. Just put it in the document, and it'll
|
||||
* spin. You can configure the following using CSS custom properties:
|
||||
*
|
||||
* --size: Size of the spinner element (it's always square). Default: 28px.
|
||||
* --color: Color of the spinner. Default: #4285f4.
|
||||
* --stroke-width: Width of the stroke of the spinner. Default: 3px.
|
||||
* --delay: Once the spinner enters the DOM, how long until it shows. This prevents the spinner
|
||||
* appearing on the screen for short operations. Default: 300ms.
|
||||
*/
|
||||
export default class LoadingSpinner extends HTMLElement {
|
||||
private _delayTimeout: number = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Ideally we'd use shadow DOM here, but we're targeting browsers without shadow DOM support.
|
||||
// You can't set attributes/content in a custom element constructor, so I'm waiting a microtask.
|
||||
Promise.resolve().then(() => {
|
||||
this.style.display = 'none';
|
||||
this.innerHTML = '' +
|
||||
`<div class="${styles.spinnerContainer}">` +
|
||||
`<div class="${styles.spinnerLayer}">` +
|
||||
`<div class="${styles.spinnerCircleClipper} ${styles.spinnerLeft}">` +
|
||||
`<div class="${styles.spinnerCircle}"></div>` +
|
||||
'</div>' +
|
||||
`<div class="${styles.spinnerGapPatch}">` +
|
||||
`<div class="${styles.spinnerCircle}"></div>` +
|
||||
'</div>' +
|
||||
`<div class="${styles.spinnerCircleClipper} ${styles.spinnerRight}">` +
|
||||
`<div class="${styles.spinnerCircle}"></div>` +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.style.display = 'none';
|
||||
clearTimeout(this._delayTimeout);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const delayStr = getComputedStyle(this).getPropertyValue('--delay').trim();
|
||||
let delayNum = parseFloat(delayStr);
|
||||
|
||||
// If seconds…
|
||||
if (/\ds$/.test(delayStr)) {
|
||||
// Convert to ms.
|
||||
delayNum *= 1000;
|
||||
}
|
||||
|
||||
this._delayTimeout = self.setTimeout(
|
||||
() => { this.style.display = ''; },
|
||||
delayNum,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('loading-spinner', LoadingSpinner);
|
||||
7
src/components/custom-els/LoadingSpinner/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
interface LoadingSpinner extends JSX.HTMLAttributes {}
|
||||
|
||||
declare namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
'loading-spinner': LoadingSpinner;
|
||||
}
|
||||
}
|
||||
124
src/components/custom-els/LoadingSpinner/styles.css
Normal file
@@ -0,0 +1,124 @@
|
||||
@keyframes spinner-left-spin {
|
||||
from { transform: rotate(130deg); }
|
||||
50% { transform: rotate(-5deg); }
|
||||
to { transform: rotate(130deg); }
|
||||
}
|
||||
|
||||
@keyframes spinner-right-spin {
|
||||
from { transform: rotate(-130deg); }
|
||||
50% { transform: rotate(5deg); }
|
||||
to { transform: rotate(-130deg); }
|
||||
}
|
||||
|
||||
@keyframes spinner-fade-out {
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes spinner-container-rotate {
|
||||
to { transform: rotate(360deg) }
|
||||
}
|
||||
|
||||
@keyframes spinner-fill-unfill-rotate {
|
||||
12.5% { transform: rotate(135deg); } /* 0.5 * ARCSIZE */
|
||||
25% { transform: rotate(270deg); } /* 1 * ARCSIZE */
|
||||
37.5% { transform: rotate(405deg); } /* 1.5 * ARCSIZE */
|
||||
50% { transform: rotate(540deg); } /* 2 * ARCSIZE */
|
||||
62.5% { transform: rotate(675deg); } /* 2.5 * ARCSIZE */
|
||||
75% { transform: rotate(810deg); } /* 3 * ARCSIZE */
|
||||
87.5% { transform: rotate(945deg); } /* 3.5 * ARCSIZE */
|
||||
to { transform: rotate(1080deg); } /* 4 * ARCSIZE */
|
||||
}
|
||||
|
||||
loading-spinner {
|
||||
--size: 28px;
|
||||
--color: #4285f4;
|
||||
--stroke-width: 3px;
|
||||
--delay: 300ms;
|
||||
|
||||
pointer-events: none;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-color: var(--color);
|
||||
}
|
||||
|
||||
loading-spinner .spinner-circle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
width: 200%;
|
||||
border-width: var(--stroke-width);
|
||||
border-style: solid;
|
||||
border-color: inherit;
|
||||
border-bottom-color: transparent !important;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/*
|
||||
Patch the gap that appear between the two adjacent div.circle-clipper while the
|
||||
spinner is rotating (appears on Chrome 38, Safari 7.1, and IE 11).
|
||||
*/
|
||||
loading-spinner .spinner-gap-patch {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
top: 0;
|
||||
left: 45%;
|
||||
width: 10%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
loading-spinner .spinner-gap-patch .spinner-circle {
|
||||
width: 1000%;
|
||||
left: -450%;
|
||||
}
|
||||
|
||||
loading-spinner .spinner-circle-clipper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
loading-spinner .spinner-left .spinner-circle {
|
||||
border-right-color: transparent !important;
|
||||
transform: rotate(129deg);
|
||||
animation: spinner-left-spin 1333ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
|
||||
}
|
||||
|
||||
loading-spinner .spinner-right .spinner-circle {
|
||||
left: -100%;
|
||||
border-left-color: transparent !important;
|
||||
transform: rotate(-129deg);
|
||||
animation: spinner-right-spin 1333ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
|
||||
}
|
||||
|
||||
loading-spinner.spinner-fadeout {
|
||||
animation: spinner-fade-out 400ms cubic-bezier(0.4, 0.0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
loading-spinner .spinner-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-color: inherit;
|
||||
|
||||
/* duration: 360 * ARCTIME / (ARCSTARTROT + (360-ARCSIZE)) */
|
||||
animation: spinner-container-rotate 1568ms linear infinite;
|
||||
}
|
||||
|
||||
loading-spinner .spinner-layer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-color: inherit;
|
||||
/* durations: 4 * ARCTIME */
|
||||
animation: spinner-fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
|
||||
}
|
||||
BIN
src/components/intro/imgs/demos/artwork-icon.jpg
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src/components/intro/imgs/demos/artwork.jpg
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
src/components/intro/imgs/demos/device-screen-icon.jpg
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/components/intro/imgs/demos/device-screen.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
src/components/intro/imgs/demos/large-photo-icon.jpg
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src/components/intro/imgs/demos/large-photo.jpg
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
src/components/intro/imgs/demos/logo-icon.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
1
src/components/intro/imgs/logo.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
@@ -1,26 +1,134 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { bind, linkRef, Fileish } from '../../lib/util';
|
||||
import '../custom-els/LoadingSpinner';
|
||||
|
||||
import logo from './imgs/logo.svg';
|
||||
import largePhoto from './imgs/demos/large-photo.jpg';
|
||||
import artwork from './imgs/demos/artwork.jpg';
|
||||
import deviceScreen from './imgs/demos/device-screen.png';
|
||||
import largePhotoIcon from './imgs/demos/large-photo-icon.jpg';
|
||||
import artworkIcon from './imgs/demos/artwork-icon.jpg';
|
||||
import deviceScreenIcon from './imgs/demos/device-screen-icon.jpg';
|
||||
import logoIcon from './imgs/demos/logo-icon.png';
|
||||
import * as style from './style.scss';
|
||||
import { bind } from '../../lib/util';
|
||||
|
||||
const demos = [
|
||||
{
|
||||
description: 'Large photo (2.8mb)',
|
||||
filename: 'photo.jpg',
|
||||
url: largePhoto,
|
||||
iconUrl: largePhotoIcon,
|
||||
},
|
||||
{
|
||||
description: 'Artwork (2.9mb)',
|
||||
filename: 'art.jpg',
|
||||
url: artwork,
|
||||
iconUrl: artworkIcon,
|
||||
},
|
||||
{
|
||||
description: 'Device screen (1.6mb)',
|
||||
filename: 'pixel3.png',
|
||||
url: deviceScreen,
|
||||
iconUrl: deviceScreenIcon,
|
||||
},
|
||||
{
|
||||
description: 'SVG icon (13k)',
|
||||
filename: 'squoosh.svg',
|
||||
url: logo,
|
||||
iconUrl: logoIcon,
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
onFile: (file: File) => void;
|
||||
onFile: (file: File | Fileish) => void;
|
||||
onError: (error: string) => void;
|
||||
}
|
||||
interface State {
|
||||
fetchingDemoIndex?: number;
|
||||
}
|
||||
interface State {}
|
||||
|
||||
export default class Intro extends Component<Props, State> {
|
||||
state: State = {};
|
||||
private fileInput?: HTMLInputElement;
|
||||
|
||||
@bind
|
||||
onFileChange(event: Event): void {
|
||||
private onFileChange(event: Event): void {
|
||||
const fileInput = event.target as HTMLInputElement;
|
||||
const file = fileInput.files && fileInput.files[0];
|
||||
if (!file) return;
|
||||
this.props.onFile(file);
|
||||
}
|
||||
|
||||
render({ }: Props, { }: State) {
|
||||
@bind
|
||||
private onButtonClick() {
|
||||
this.fileInput!.click();
|
||||
}
|
||||
|
||||
@bind
|
||||
private async onDemoClick(index: number, event: Event) {
|
||||
try {
|
||||
this.setState({ fetchingDemoIndex: index });
|
||||
const demo = demos[index];
|
||||
const blob = await fetch(demo.url).then(r => r.blob());
|
||||
|
||||
// Firefox doesn't like content types like 'image/png; charset=UTF-8', which Webpack's dev
|
||||
// server returns. https://bugzilla.mozilla.org/show_bug.cgi?id=1497925.
|
||||
const type = /[^;]*/.exec(blob.type)![0];
|
||||
const file = new Fileish([blob], demo.filename, { type });
|
||||
this.props.onFile(file);
|
||||
} catch (err) {
|
||||
this.setState({ fetchingDemoIndex: undefined });
|
||||
this.props.onError("Couldn't fetch demo image");
|
||||
}
|
||||
}
|
||||
|
||||
render({ }: Props, { fetchingDemoIndex }: State) {
|
||||
return (
|
||||
<div class={style.welcome}>
|
||||
<h1>Drop, paste or select an image</h1>
|
||||
<input type="file" onChange={this.onFileChange} />
|
||||
<div class={style.intro}>
|
||||
<div>
|
||||
<div class={style.logoSizer}>
|
||||
<div class={style.logoContainer}>
|
||||
<img src={logo} class={style.logo} alt="Squoosh" />
|
||||
</div>
|
||||
</div>
|
||||
<p class={style.openImageGuide}>
|
||||
Drag & drop or{' '}
|
||||
<button class={style.selectButton} onClick={this.onButtonClick}>select an image</button>
|
||||
<input
|
||||
class={style.hide}
|
||||
ref={linkRef(this, 'fileInput')}
|
||||
type="file"
|
||||
onChange={this.onFileChange}
|
||||
/>
|
||||
</p>
|
||||
<p>Or try one of these:</p>
|
||||
<ul class={style.demos}>
|
||||
{demos.map((demo, i) =>
|
||||
<li key={demo.url} class={style.demoItem}>
|
||||
<button class={style.demoButton} onClick={this.onDemoClick.bind(this, i)}>
|
||||
<div class={style.demo}>
|
||||
<div class={style.demoImgContainer}>
|
||||
<div class={style.demoImgAspect}>
|
||||
<img class={style.demoIcon} src={demo.iconUrl} alt=""/>
|
||||
{fetchingDemoIndex === i &&
|
||||
<div class={style.demoLoading}>
|
||||
<loading-spinner class={style.demoLoadingSpinner}/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class={style.demoDescription}>{demo.description}</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>,
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<ul class={style.relatedLinks}>
|
||||
<li><a href="https://github.com/GoogleChromeLabs/squoosh/">View the code</a></li>
|
||||
<li><a href="https://github.com/GoogleChromeLabs/squoosh/issues">Report a bug</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,191 @@
|
||||
.welcome {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
font-weight: inherit;
|
||||
font-size: 150%;
|
||||
text-align: center;
|
||||
@font-face {
|
||||
font-family: 'intro-text';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
// This only contains the chars for "Drag & drop or"
|
||||
src: url('data:font/woff2;base64,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') format('woff2');
|
||||
}
|
||||
|
||||
input {
|
||||
display: inline-block;
|
||||
width: 16em;
|
||||
padding: 10px;
|
||||
@font-face {
|
||||
font-family: 'intro-text';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
// Only contains the chars for "select an image"
|
||||
src: url('data:font/woff2;base64,d09GMgABAAAAAAXMAA4AAAAACwQAAAV5AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbg1IcMAZgAGwRCAqJUId/CxoAATYCJAMwBCAFgnAHIBskCcjEh6dNff8Ou/9Tj9VZGnUhJeqFzWGiVVOkxkTthr9f6kWkRdsBbkIj3YuLaloFZWr7aBg22z7IOoWqBWCW5cZU3GBrh0n+dAcBYAUlzYHAzWTYchqKXEyAT0zOLIS1qqm+B8q2ur4OhEMy0PHHUH8KklaSr8T0mp/EU7kRvXlI1E09HXA1qLN8Djxa0AsSDOg3cJARb9mtQJoSK3gvEn372/gcAigg/gOnbsT/MYv491GTReW4rJC5LA+h5FFclF6QQgoZ5Kx7GbsuGeytUgFClkOomY2Gdake3m9HegkHieAx/a0hBTALsy4jvpxBcnFXUnjC+2ZS5zHnDeEaJVwi+ZWqzOm4Uvgy4k6kGv4kFDVkfjk1gkVRRkk2zlo42PBbRJmG30cClJQjak7BnfQqHza4ITKftQZ/ZMUaEiyy1+mYCh4clKhDA5rQglZ0oG9jw+qiNvT+SfxIXCeuFdeIq8VV4gpxyRaGOl0JiChiCocfc7e93DwZIPvWgPiZJLcJugxyjW7UQyl4TJk6dWqYU7Cn0WQiWnNJCdGeprqjW63fpVS3mKy4YGZ6I3ya4nbIVgM1mwkpNEBzixlNxfPmH7owvdE4973OM9quvk11dwvnzDUy/Zn5S5Ywpn/PeqXBQI2m4lna05CRtsI6+GIENjS9K4jWRHUGYA2ozdZm2Smmf0DI3aqpeNbsJfxe7YdFmcZAn5gXLCFa2/Umqiu017APFhMZ0rfQp4sJX0ZrJ+n9UtAljr5VYWb6oj1MrpvX3qe6u8WRJg0bj7aPkDOa7m+E0Oa9Y8eY/gbRbr+efH7hcO49bMd28fbDVHcUmm3XkozQGKjeBHSJ4TQnI879LIFmF2v/BJuEQlffJPfE9oKayS/PsPE44fvM4MsBESxbuEEV39d5pw6oW4vD6S1WQC3UpSbHNbK0Jikl0bphSs+0CGW8Ew4Kzw7zarmcVz873JHTFhKYay18R8vY0ozPiHPAGyROAqlW5fLj5+HbWBn9TpgekKsOy8N+4dlFfL9i/Nk3+gY1bwzZUAvLVNiFpvqHRenetSoVrgn2obGtPsltEVxEeHJAQFhyBIcHT9rDvTJm06e0TLgase2gd2RffGJNg0o1zrdRyi9s1bYE5bi85cK+o/nUwvBR5+jweEBaSMoCub29fEFISmib9Dn5yl5kVFpoGrPQSmZhafQ7WimttNCH7Cktohb6kFpoEfIsdDF7SgvZTbaY3mKFyLXQh+wpzWE3mUNQQkWnR+lgiW9Afkunej49Nz3sYlI8XFTRdkNhUR5d6h4oOpJc8OjcItMXVqoHW2fSW6ycWuiM8NDYoICouLAZ9BYrwwmhycvKlt4Q8hUlCV5nZ7vOm2ut2rizcFpuWpSrT3K1Z3xinbuHnXBTyGAljV/XzHZaNGu6y7vLDziMpIyUdOBBRXXlxznUQiOoheZsZk9njG8er1mNmz2eOCQ3x9BbLP+Zxt+VrbEz9aWxmimRvyl4/sumyoM/nw+LNzV4/uP0/9T/P5f08cvhl38USAS/xTYs2fL/VNFF0vd+SVsRB/khPwW4SCi5SHhx8fDgVPAiAqIJRQL/EuK5bsRzNnAiezD1i7u2VyHHAKRU+E2YUaA5DUsE2ZfbApAmsJcxjBwmQ2Xk4Y2BqZJ+oxSzsNEogz1O3/xkBMKEBHSiC8PoQStaoEIflPCHL/wQBCUKoUITlMhHP+olt5ojykUPOrEQTWgY+f449KMPKnSiB72jGrLQhEZO24925LtbkG5DndTkD2/4um+NQBEyUIJsRBxX24tcDGmbWtGJjq05jhzTaMgCcR+6EA4f+KAXDay5u6RsL7xJsm3w3mzvFvggB8nI/AYBdVnxNvx/2wA=') format('woff2');
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0 }
|
||||
}
|
||||
|
||||
.intro {
|
||||
composes: abs-fill from '../../lib/util.scss';
|
||||
display: grid;
|
||||
grid-template-rows: 1fr min-content;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
text-align: center;
|
||||
font-size: 2rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow: auto;
|
||||
padding: 20px 0 0;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
position: relative;
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
.logo-sizer {
|
||||
width: 90%;
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
-webkit-appearance: none;
|
||||
border: 1px solid var(--button-fg);
|
||||
background: rgba(var(--button-fg-color), 0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
composes: abs-fill from '../../lib/util.scss';
|
||||
}
|
||||
|
||||
.open-image-guide {
|
||||
font: 300 11vw intro-text;
|
||||
margin-bottom: 0;
|
||||
|
||||
@media (min-width: 460px) {
|
||||
font-size: 50.6px;
|
||||
padding: 0 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.select-button {
|
||||
composes: unbutton from '../../lib/util.scss';
|
||||
font-weight: 500;
|
||||
color: #5D509E;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.demos {
|
||||
display: block;
|
||||
padding: 0;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (min-width: 400px) {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 580px) {
|
||||
border-top: none;
|
||||
width: 523px;
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
width: 773px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
background: #fff;
|
||||
display: flex;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
|
||||
@media (min-width: 580px) {
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 4px;
|
||||
margin: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
composes: unbutton from '../../lib/util.scss';
|
||||
flex: 1;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.demo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 7px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.demo-img-container {
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
width: 47px;
|
||||
background: #ccc;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.demo-img-aspect {
|
||||
position: relative;
|
||||
padding-top: 100%;
|
||||
|
||||
}
|
||||
|
||||
.demo-icon {
|
||||
composes: abs-fill from '../../lib/util.scss';
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.demo-loading {
|
||||
composes: abs-fill from '../../lib/util.scss';
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fade-in 300ms ease-in-out;
|
||||
}
|
||||
|
||||
.demo-loading-spinner {
|
||||
--color: #fff;
|
||||
}
|
||||
|
||||
.related-links {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
font-size: 1.3rem;
|
||||
|
||||
& li {
|
||||
display: block;
|
||||
border-left: 1px solid #000;
|
||||
padding: 0 0.6em;
|
||||
|
||||
&:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
& a:link {
|
||||
color: #5D509E;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
src/lib/util.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
.abs-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.unbutton {
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
font: inherit;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
40
src/missing-types.d.ts
vendored
@@ -1,22 +1,24 @@
|
||||
// PRs to fix this:
|
||||
// https://github.com/developit/preact/pull/1101
|
||||
// https://github.com/developit/preact/pull/1102
|
||||
declare namespace JSX {
|
||||
type PointerEventHandler = EventHandler<PointerEvent>;
|
||||
|
||||
interface DOMAttributes {
|
||||
onTouchStartCapture?: TouchEventHandler;
|
||||
onTouchEndCapture?: TouchEventHandler;
|
||||
onTouchMoveCapture?: TouchEventHandler;
|
||||
|
||||
onPointerDownCapture?: PointerEventHandler;
|
||||
|
||||
onMouseDownCapture?: MouseEventHandler;
|
||||
|
||||
onWheelCapture?: WheelEventHandler;
|
||||
}
|
||||
}
|
||||
|
||||
interface CanvasRenderingContext2D {
|
||||
filter: string;
|
||||
}
|
||||
|
||||
// Handling file-loader imports:
|
||||
declare module '*.png' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.gif' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ html, body {
|
||||
overflow: hidden;
|
||||
overscroll-behavior: none;
|
||||
contain: strict;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path d="M1 2V0h1v1H0v1z" fill-opacity=".025"/></svg>');
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@@ -156,6 +156,10 @@ module.exports = function (_, env) {
|
||||
// See https://github.com/webpack/webpack/issues/6725
|
||||
type: 'javascript/auto',
|
||||
loader: 'file-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(png|svg|jpg|gif)$/,
|
||||
loader: 'file-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||