Prerender & client render of intro
@ -16,6 +16,7 @@
|
||||
"paths": {
|
||||
"static-build/*": ["src/static-build/*"],
|
||||
"client/*": ["src/client/*"],
|
||||
"shared/*": ["src/shared/*"],
|
||||
"features/*": ["src/features/*"]
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ export default function (resolveFileUrl) {
|
||||
}),
|
||||
postCSSUrl({
|
||||
url: ({ relativePath, url }) => {
|
||||
if (/^https?:\/\//.test(url)) return url;
|
||||
if (/^(https?|data):/.test(url)) return url;
|
||||
const parsedPath = parsePath(relativePath);
|
||||
const source = readFileSync(
|
||||
resolvePath(dirname(path), relativePath),
|
||||
|
53
lib/initial-css-plugin.js
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { promisify } from 'util';
|
||||
|
||||
import path from 'path';
|
||||
import glob from 'glob';
|
||||
|
||||
const globP = promisify(glob);
|
||||
|
||||
const moduleId = 'initial-css:';
|
||||
|
||||
export default function initialCssPlugin() {
|
||||
return {
|
||||
name: 'initial-css-plugin',
|
||||
resolveId(id) {
|
||||
if (id === moduleId) return moduleId;
|
||||
},
|
||||
async load(id) {
|
||||
if (id !== moduleId) return;
|
||||
|
||||
const matches = await globP('shared/initial-app/**/*.css', {
|
||||
nodir: true,
|
||||
cwd: path.join(process.cwd(), 'src'),
|
||||
});
|
||||
|
||||
// Sort the matches so the parentmost items appear first.
|
||||
// This is a bit of a hack, but it means the util stuff appears in the cascade first.
|
||||
const sortedMatches = matches
|
||||
.map((match) => match.split(path.sep))
|
||||
.sort((a, b) => a.length - b.length)
|
||||
.map((match) => path.join(...match));
|
||||
|
||||
const imports = sortedMatches
|
||||
.map((id, i) => `import css${i} from 'css:${id}';\n`)
|
||||
.join('');
|
||||
|
||||
return (
|
||||
imports +
|
||||
`export default ${sortedMatches.map((_, i) => `css${i}`).join(' + ')};`
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
5
missing-types.d.ts
vendored
@ -27,4 +27,9 @@ declare module 'css:*' {
|
||||
export default source;
|
||||
}
|
||||
|
||||
declare var ga: {
|
||||
(...args: any[]): void;
|
||||
q: any[];
|
||||
};
|
||||
|
||||
declare const __PRODUCTION__: boolean;
|
||||
|
@ -23,11 +23,12 @@ import simpleTS from './lib/simple-ts';
|
||||
import clientBundlePlugin from './lib/client-bundle-plugin';
|
||||
import nodeExternalPlugin from './lib/node-external-plugin';
|
||||
import cssPlugin from './lib/css-plugin';
|
||||
import assetPlugin from './lib/asset-plugin';
|
||||
import urlPlugin from './lib/url-plugin';
|
||||
import resolveDirsPlugin from './lib/resolve-dirs-plugin';
|
||||
import runScript from './lib/run-script';
|
||||
import emitFiles from './lib/emit-files-plugin';
|
||||
import imageWorkerPlugin from './lib/image-worker-plugin';
|
||||
import initialCssPlugin from './lib/initial-css-plugin';
|
||||
|
||||
function resolveFileUrl({ fileName }) {
|
||||
return JSON.stringify(fileName.replace(/^static\//, '/'));
|
||||
@ -57,10 +58,11 @@ export default async function ({ watch }) {
|
||||
resolveDirsPlugin([
|
||||
'src/static-build',
|
||||
'src/client',
|
||||
'src/shared',
|
||||
'src/features',
|
||||
'codecs',
|
||||
]),
|
||||
assetPlugin(),
|
||||
urlPlugin(),
|
||||
cssPlugin(resolveFileUrl),
|
||||
];
|
||||
const dir = '.tmp/build';
|
||||
@ -104,7 +106,8 @@ export default async function ({ watch }) {
|
||||
nodeExternalPlugin(),
|
||||
imageWorkerPlugin(),
|
||||
replace({ __PRERENDER__: true, __PRODUCTION__: isProduction }),
|
||||
runScript(dir + '/index.js'),
|
||||
initialCssPlugin(),
|
||||
runScript(dir + '/static-build/index.js'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
import type { FileDropEvent } from 'file-drop-element';
|
||||
import type SnackBarElement from 'client/initial-app/custom-els/snack-bar';
|
||||
import type { SnackOptions } from 'client/initial-app/custom-els/snack-bar';
|
||||
import type SnackBarElement from 'shared/initial-app/custom-els/snack-bar';
|
||||
import type { SnackOptions } from 'shared/initial-app/custom-els/snack-bar';
|
||||
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { linkRef } from 'client/initial-app/util';
|
||||
import { linkRef } from 'shared/initial-app/util';
|
||||
import * as style from './style.css';
|
||||
import 'add-css:./style.css';
|
||||
import 'file-drop-element';
|
||||
import 'client/initial-app/custom-els/snack-bar';
|
||||
//import Intro from '../intro';
|
||||
import 'client/initial-app/custom-els/loading-spinner';
|
||||
import 'shared/initial-app/custom-els/snack-bar';
|
||||
import Intro from 'shared/initial-app/Intro';
|
||||
import 'shared/initial-app/custom-els/loading-spinner';
|
||||
|
||||
const ROUTE_EDITOR = '/editor';
|
||||
|
||||
@ -127,8 +127,7 @@ export default class App extends Component<Props, State> {
|
||||
//<Compress file={file!} showSnack={this.showSnack} onBack={back} />
|
||||
'TODO: uncomment above'
|
||||
) : (
|
||||
//<Intro onFile={this.onIntroPickFile} showSnack={this.showSnack} />
|
||||
'TODO: show intro here'
|
||||
<Intro onFile={this.onIntroPickFile} showSnack={this.showSnack} />
|
||||
)}
|
||||
<snack-bar ref={linkRef(this, 'snackbar')} />
|
||||
</file-drop>
|
||||
|
@ -1,3 +1,5 @@
|
||||
/// <reference path="../../../shared/initial-app/custom-els/snack-bar/missing-types.d.ts" />
|
||||
/// <reference path="../../../shared/initial-app/custom-els/loading-spinner/missing-types.d.ts" />
|
||||
import type { FileDropElement, FileDropEvent } from 'file-drop-element';
|
||||
|
||||
interface FileDropAttributes extends preact.JSX.HTMLAttributes {
|
||||
|
6
src/client/missing-types.d.ts
vendored
@ -11,11 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../missing-types.d.ts" />
|
||||
|
||||
declare var ga: {
|
||||
(...args: any[]): void;
|
||||
q: any[];
|
||||
};
|
||||
/// <reference path="../shared/initial-app/Intro/missing-types.d.ts" />
|
||||
|
||||
interface Navigator {
|
||||
readonly standalone: boolean;
|
||||
|
@ -4,5 +4,5 @@
|
||||
"lib": ["esnext", "dom", "dom.iterable"],
|
||||
"types": []
|
||||
},
|
||||
"references": [{ "path": "../features/worker" }]
|
||||
"references": [{ "path": "../features/worker" }, { "path": "../shared" }]
|
||||
}
|
||||
|
BIN
src/shared/initial-app/Intro/imgs/demos/demo-artwork.jpg
Normal file
After Width: | Height: | Size: 2.8 MiB |
BIN
src/shared/initial-app/Intro/imgs/demos/demo-device-screen.png
Normal file
After Width: | Height: | Size: 1.5 MiB |
BIN
src/shared/initial-app/Intro/imgs/demos/demo-large-photo.jpg
Normal file
After Width: | Height: | Size: 2.7 MiB |
BIN
src/shared/initial-app/Intro/imgs/demos/icon-demo-artwork.jpg
Normal file
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 4.3 KiB |
BIN
src/shared/initial-app/Intro/imgs/demos/icon-demo-logo.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
1
src/shared/initial-app/Intro/imgs/logo.svg
Normal file
After Width: | Height: | Size: 13 KiB |
255
src/shared/initial-app/Intro/index.tsx
Normal file
@ -0,0 +1,255 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { linkRef } from 'shared/initial-app/util';
|
||||
import '../custom-els/loading-spinner';
|
||||
|
||||
import logo from 'url:./imgs/logo.svg';
|
||||
import largePhoto from 'url:./imgs/demos/demo-large-photo.jpg';
|
||||
import artwork from 'url:./imgs/demos/demo-artwork.jpg';
|
||||
import deviceScreen from 'url:./imgs/demos/demo-device-screen.png';
|
||||
import largePhotoIcon from 'url:./imgs/demos/icon-demo-large-photo.jpg';
|
||||
import artworkIcon from 'url:./imgs/demos/icon-demo-artwork.jpg';
|
||||
import deviceScreenIcon from 'url:./imgs/demos/icon-demo-device-screen.jpg';
|
||||
import logoIcon from 'url:./imgs/demos/icon-demo-logo.png';
|
||||
import * as style from './style.css';
|
||||
import type SnackBarElement from 'shared/initial-app/custom-els/snack-bar';
|
||||
import 'shared/initial-app/custom-els/snack-bar';
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
const installButtonSource = 'introInstallButton-Purple';
|
||||
|
||||
interface Props {
|
||||
onFile?: (file: File) => void;
|
||||
showSnack?: SnackBarElement['showSnackbar'];
|
||||
}
|
||||
interface State {
|
||||
fetchingDemoIndex?: number;
|
||||
beforeInstallEvent?: BeforeInstallPromptEvent;
|
||||
}
|
||||
|
||||
export default class Intro extends Component<Props, State> {
|
||||
state: State = {};
|
||||
private fileInput?: HTMLInputElement;
|
||||
private installingViaButton = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (__PRERENDER__) return;
|
||||
// Listen for beforeinstallprompt events, indicating Squoosh is installable.
|
||||
window.addEventListener(
|
||||
'beforeinstallprompt',
|
||||
this.onBeforeInstallPromptEvent,
|
||||
);
|
||||
|
||||
// Listen for the appinstalled event, indicating Squoosh has been installed.
|
||||
window.addEventListener('appinstalled', this.onAppInstalled);
|
||||
}
|
||||
|
||||
private resetFileInput = () => {
|
||||
this.fileInput!.value = '';
|
||||
};
|
||||
|
||||
private onFileChange = (event: Event): void => {
|
||||
const fileInput = event.target as HTMLInputElement;
|
||||
const file = fileInput.files && fileInput.files[0];
|
||||
if (!file) return;
|
||||
this.resetFileInput();
|
||||
this.props.onFile!(file);
|
||||
};
|
||||
|
||||
private onButtonClick = () => {
|
||||
this.fileInput!.click();
|
||||
};
|
||||
|
||||
private onDemoClick = async (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 File([blob], demo.filename, { type });
|
||||
this.props.onFile!(file);
|
||||
} catch (err) {
|
||||
this.setState({ fetchingDemoIndex: undefined });
|
||||
this.props.showSnack!("Couldn't fetch demo image");
|
||||
}
|
||||
};
|
||||
|
||||
private onBeforeInstallPromptEvent = (event: BeforeInstallPromptEvent) => {
|
||||
// Don't show the mini-infobar on mobile
|
||||
event.preventDefault();
|
||||
|
||||
// Save the beforeinstallprompt event so it can be called later.
|
||||
this.setState({ beforeInstallEvent: event });
|
||||
|
||||
// Log the event.
|
||||
const gaEventInfo = {
|
||||
eventCategory: 'pwa-install',
|
||||
eventAction: 'promo-shown',
|
||||
nonInteraction: true,
|
||||
};
|
||||
ga('send', 'event', gaEventInfo);
|
||||
};
|
||||
|
||||
private onInstallClick = async (event: Event) => {
|
||||
// Get the deferred beforeinstallprompt event
|
||||
const beforeInstallEvent = this.state.beforeInstallEvent;
|
||||
// If there's no deferred prompt, bail.
|
||||
if (!beforeInstallEvent) return;
|
||||
|
||||
this.installingViaButton = true;
|
||||
|
||||
// Show the browser install prompt
|
||||
beforeInstallEvent.prompt();
|
||||
|
||||
// Wait for the user to accept or dismiss the install prompt
|
||||
const { outcome } = await beforeInstallEvent.userChoice;
|
||||
// Send the analytics data
|
||||
const gaEventInfo = {
|
||||
eventCategory: 'pwa-install',
|
||||
eventAction: 'promo-clicked',
|
||||
eventLabel: installButtonSource,
|
||||
eventValue: outcome === 'accepted' ? 1 : 0,
|
||||
};
|
||||
ga('send', 'event', gaEventInfo);
|
||||
|
||||
// If the prompt was dismissed, we aren't going to install via the button.
|
||||
if (outcome === 'dismissed') {
|
||||
this.installingViaButton = false;
|
||||
}
|
||||
};
|
||||
|
||||
private onAppInstalled = () => {
|
||||
// We don't need the install button, if it's shown
|
||||
this.setState({ beforeInstallEvent: undefined });
|
||||
|
||||
// Don't log analytics if page is not visible
|
||||
if (document.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to get the install, if it's not set, use 'browser'
|
||||
const source = this.installingViaButton ? installButtonSource : 'browser';
|
||||
ga('send', 'event', 'pwa-install', 'installed', source);
|
||||
|
||||
// Clear the install method property
|
||||
this.installingViaButton = false;
|
||||
};
|
||||
|
||||
render({}: Props, { fetchingDemoIndex, beforeInstallEvent }: State) {
|
||||
return (
|
||||
<div class={style.intro}>
|
||||
<div>
|
||||
<div class={style.logoSizer}>
|
||||
<div class={style.logoContainer}>
|
||||
<img
|
||||
src={logo}
|
||||
class={style.logo}
|
||||
alt="Squoosh"
|
||||
decoding="async"
|
||||
/>
|
||||
</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=""
|
||||
decoding="async"
|
||||
/>
|
||||
{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>
|
||||
{beforeInstallEvent && (
|
||||
<button
|
||||
type="button"
|
||||
class={style.installButton}
|
||||
onClick={this.onInstallClick}
|
||||
>
|
||||
Install
|
||||
</button>
|
||||
)}
|
||||
<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>
|
||||
<li>
|
||||
<a href="https://github.com/GoogleChromeLabs/squoosh/blob/dev/README.md#privacy">
|
||||
Privacy
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
31
src/shared/initial-app/Intro/missing-types.d.ts
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* The BeforeInstallPromptEvent is fired at the Window.onbeforeinstallprompt handler
|
||||
* before a user is prompted to "install" a web site to a home screen on mobile.
|
||||
*/
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
/**
|
||||
* Returns an array of DOMString items containing the platforms on which the event was dispatched.
|
||||
* This is provided for user agents that want to present a choice of versions to the user such as,
|
||||
* for example, "web" or "play" which would allow the user to chose between a web version or
|
||||
* an Android version.
|
||||
*/
|
||||
readonly platforms: Array<string>;
|
||||
|
||||
/**
|
||||
* Returns a Promise that resolves to a DOMString containing either "accepted" or "dismissed".
|
||||
*/
|
||||
readonly userChoice: Promise<{
|
||||
outcome: 'accepted' | 'dismissed';
|
||||
platform: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Allows a developer to show the install prompt at a time of their own choosing.
|
||||
* This method returns a Promise.
|
||||
*/
|
||||
prompt(): Promise<void>;
|
||||
}
|
||||
|
||||
interface WindowEventMap {
|
||||
beforeinstallprompt: BeforeInstallPromptEvent;
|
||||
}
|
228
src/shared/initial-app/Intro/style.css
Normal file
@ -0,0 +1,228 @@
|
||||
@font-face {
|
||||
font-family: 'intro-text';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: block;
|
||||
/* This only contains the chars for "Drag & drop or" */
|
||||
src: url('data:font/woff2;base64,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')
|
||||
format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'intro-text';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: block;
|
||||
/* 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 {
|
||||
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;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
overscroll-behavior: contain;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
position: relative;
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
.logo-sizer {
|
||||
width: 90%;
|
||||
max-width: 52vh;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
composes: abs-fill from '../util.css';
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.open-image-guide {
|
||||
font: 300 11vw intro-text, sans-serif;
|
||||
margin-bottom: 0;
|
||||
|
||||
@media (min-width: 460px) {
|
||||
font-size: 50.6px;
|
||||
padding: 0 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.select-button {
|
||||
composes: unbutton from '../util.css';
|
||||
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 '../util.css';
|
||||
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;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.demo-img-aspect {
|
||||
position: relative;
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
.demo-icon {
|
||||
composes: abs-fill from '../util.css';
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.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 '../util.css';
|
||||
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;
|
||||
}
|
||||
|
||||
.install-button {
|
||||
composes: unbutton from '../util.css';
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: #504488;
|
||||
}
|
||||
|
||||
background: #5d509e;
|
||||
border: 1px solid #e8e8e8;
|
||||
color: #fff;
|
||||
padding: 14px;
|
||||
font-size: 1.3rem;
|
||||
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
|
||||
animation: fade-in 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,10 @@
|
||||
import * as styles from './styles.css';
|
||||
|
||||
// So it doesn't cause an error when running in node
|
||||
const HTMLEl = ((__PRERENDER__
|
||||
? Object
|
||||
: HTMLElement) as unknown) as typeof HTMLElement;
|
||||
|
||||
/**
|
||||
* 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:
|
||||
@ -10,11 +15,12 @@ import * as styles from './styles.css';
|
||||
* --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 {
|
||||
export default class LoadingSpinner extends HTMLEl {
|
||||
private _delayTimeout: number = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (!__PRERENDER__) return;
|
||||
|
||||
// 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.
|
||||
@ -59,4 +65,4 @@ export default class LoadingSpinner extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('loading-spinner', LoadingSpinner);
|
||||
if (!__PRERENDER__) customElements.define('loading-spinner', LoadingSpinner);
|
@ -1,5 +1,10 @@
|
||||
import * as style from './styles.css';
|
||||
|
||||
// So it doesn't cause an error when running in node
|
||||
const HTMLEl = ((__PRERENDER__
|
||||
? Object
|
||||
: HTMLElement) as unknown) as typeof HTMLElement;
|
||||
|
||||
export interface SnackOptions {
|
||||
timeout?: number;
|
||||
actions?: string[];
|
||||
@ -46,7 +51,7 @@ function createSnack(
|
||||
return [el, result];
|
||||
}
|
||||
|
||||
export default class SnackBarElement extends HTMLElement {
|
||||
export default class SnackBarElement extends HTMLEl {
|
||||
private _snackbars: [
|
||||
string,
|
||||
SnackOptions,
|
||||
@ -92,4 +97,4 @@ export default class SnackBarElement extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('snack-bar', SnackBarElement);
|
||||
if (!__PRERENDER__) customElements.define('snack-bar', SnackBarElement);
|
@ -1,4 +1,5 @@
|
||||
import type { SnackOptions } from '.';
|
||||
import type preact from 'preact';
|
||||
|
||||
interface SnackBarAttributes extends preact.JSX.HTMLAttributes {
|
||||
showSnackbar?: (options: SnackOptions) => Promise<string>;
|
22
src/shared/initial-app/util.css
Normal file
@ -0,0 +1,22 @@
|
||||
.abs-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
.unbutton {
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
font: inherit;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
11
src/shared/missing-preact-types.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
declare module 'preact' {
|
||||
namespace JSX {
|
||||
interface HTMLAttributes {
|
||||
decoding?: 'sync' | 'async' | 'auto';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Thing break unless this file is a module.
|
||||
// Don't ask me why. I don't know.
|
||||
export {};
|
15
src/shared/missing-types.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../missing-types.d.ts" />
|
||||
|
||||
declare const __PRERENDER__: boolean;
|
7
src/shared/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../generic-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext", "dom", "dom.iterable"],
|
||||
"types": []
|
||||
}
|
||||
}
|
BIN
src/static-build/assets/icon-small.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
6
src/static-build/missing-types.d.ts
vendored
@ -11,9 +11,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../missing-types.d.ts" />
|
||||
/// <reference path="../shared/initial-app/Intro/missing-types.d.ts" />
|
||||
|
||||
declare module 'client-bundle:*' {
|
||||
const url: string;
|
||||
export default url;
|
||||
export const imports: string[];
|
||||
}
|
||||
|
||||
declare module 'initial-css:' {
|
||||
const css: string;
|
||||
export default css;
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
html {
|
||||
background: green;
|
||||
}
|
55
src/static-build/pages/index/base.css
Normal file
@ -0,0 +1,55 @@
|
||||
button,
|
||||
a,
|
||||
img,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
a,
|
||||
button,
|
||||
img,
|
||||
[inert],
|
||||
.inert {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-drag: none;
|
||||
-webkit-user-drag: none;
|
||||
touch-callout: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font: 12px/1.3 system-ui, -apple-system, BlinkMacSystemFont, Roboto, Helvetica,
|
||||
sans-serif;
|
||||
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 {
|
||||
--gray-dark: rgba(0, 0, 0, 0.8);
|
||||
|
||||
--button-fg-color: 95, 180, 228;
|
||||
--button-fg: rgb(95, 180, 228);
|
||||
|
||||
--negative: rgb(207, 113, 127);
|
||||
--positive: rgb(149, 212, 159);
|
||||
}
|
||||
|
||||
:global(#app) {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
contain: strict;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
@ -12,10 +12,12 @@
|
||||
*/
|
||||
import { h, FunctionalComponent } from 'preact';
|
||||
|
||||
import css from 'css:./all.css';
|
||||
import baseCss from 'css:./base.css';
|
||||
import initialCss from 'initial-css:';
|
||||
import clientBundleURL, { imports } from 'client-bundle:client/initial-app';
|
||||
import favicon from 'url:static-build/assets/favicon.ico';
|
||||
import { escapeStyleScriptContent } from 'static-build/utils';
|
||||
import Intro from 'shared/initial-app/Intro';
|
||||
|
||||
interface Props {}
|
||||
|
||||
@ -37,7 +39,12 @@ const Index: FunctionalComponent<Props> = () => (
|
||||
<meta name="theme-color" content="#f78f21" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<style
|
||||
dangerouslySetInnerHTML={{ __html: escapeStyleScriptContent(css) }}
|
||||
dangerouslySetInnerHTML={{ __html: escapeStyleScriptContent(baseCss) }}
|
||||
/>
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: escapeStyleScriptContent(initialCss),
|
||||
}}
|
||||
/>
|
||||
<script src={clientBundleURL} defer />
|
||||
{imports.map((v) => (
|
||||
@ -45,7 +52,9 @@ const Index: FunctionalComponent<Props> = () => (
|
||||
))}
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" />
|
||||
<div id="app">
|
||||
<Intro />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
@ -4,5 +4,5 @@
|
||||
"lib": ["esnext", "dom"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"references": []
|
||||
"references": [{ "path": "../shared" }]
|
||||
}
|
||||
|
@ -4,6 +4,6 @@
|
||||
"references": [
|
||||
{ "path": "./src/client" },
|
||||
{ "path": "./src/static-build" },
|
||||
{ "path": "./src/features/worker" }
|
||||
{ "path": "./src/shared" }
|
||||
]
|
||||
}
|
||||
|