diff --git a/src/assets/favicon.ico b/src/assets/favicon.ico index a96e45b2..a3a6c2d7 100644 Binary files a/src/assets/favicon.ico and b/src/assets/favicon.ico differ diff --git a/src/assets/icon-large.png b/src/assets/icon-large.png new file mode 100644 index 00000000..e394516e Binary files /dev/null and b/src/assets/icon-large.png differ diff --git a/src/assets/icon-small.png b/src/assets/icon-small.png new file mode 100644 index 00000000..709b1829 Binary files /dev/null and b/src/assets/icon-small.png differ diff --git a/src/assets/icon.png b/src/assets/icon.png deleted file mode 100644 index 8371feaa..00000000 Binary files a/src/assets/icon.png and /dev/null differ diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index ce990fe9..92d5868b 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -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 { } @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 { // 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 { 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 { ))} : - + } {anyLoading && Loading...} diff --git a/src/components/App/style.scss b/src/components/App/style.scss index e9e8db3d..0cbb89f8 100644 --- a/src/components/App/style.scss +++ b/src/components/App/style.scss @@ -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; diff --git a/src/components/Output/style.scss b/src/components/Output/style.scss index 511d0f0f..dacb10bf 100644 --- a/src/components/Output/style.scss +++ b/src/components/Output/style.scss @@ -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,') center repeat; &:before { content: ''; diff --git a/src/components/custom-els/LoadingSpinner/index.ts b/src/components/custom-els/LoadingSpinner/index.ts new file mode 100644 index 00000000..843b1b65 --- /dev/null +++ b/src/components/custom-els/LoadingSpinner/index.ts @@ -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 = '' + + `
` + + `
` + + `
` + + `
` + + '
' + + `
` + + `
` + + '
' + + `
` + + `
` + + '
' + + '
' + + '
'; + }); + } + + 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); diff --git a/src/components/custom-els/LoadingSpinner/missing-types.d.ts b/src/components/custom-els/LoadingSpinner/missing-types.d.ts new file mode 100644 index 00000000..b9f29dc3 --- /dev/null +++ b/src/components/custom-els/LoadingSpinner/missing-types.d.ts @@ -0,0 +1,7 @@ +interface LoadingSpinner extends JSX.HTMLAttributes {} + +declare namespace JSX { + interface IntrinsicElements { + 'loading-spinner': LoadingSpinner; + } +} diff --git a/src/components/custom-els/LoadingSpinner/styles.css b/src/components/custom-els/LoadingSpinner/styles.css new file mode 100644 index 00000000..63c7856d --- /dev/null +++ b/src/components/custom-els/LoadingSpinner/styles.css @@ -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; +} diff --git a/src/components/intro/imgs/demos/artwork-icon.jpg b/src/components/intro/imgs/demos/artwork-icon.jpg new file mode 100644 index 00000000..79d05f02 Binary files /dev/null and b/src/components/intro/imgs/demos/artwork-icon.jpg differ diff --git a/src/components/intro/imgs/demos/artwork.jpg b/src/components/intro/imgs/demos/artwork.jpg new file mode 100644 index 00000000..8da9183b Binary files /dev/null and b/src/components/intro/imgs/demos/artwork.jpg differ diff --git a/src/components/intro/imgs/demos/device-screen-icon.jpg b/src/components/intro/imgs/demos/device-screen-icon.jpg new file mode 100644 index 00000000..5f140f0f Binary files /dev/null and b/src/components/intro/imgs/demos/device-screen-icon.jpg differ diff --git a/src/components/intro/imgs/demos/device-screen.png b/src/components/intro/imgs/demos/device-screen.png new file mode 100644 index 00000000..b4c237c5 Binary files /dev/null and b/src/components/intro/imgs/demos/device-screen.png differ diff --git a/src/components/intro/imgs/demos/large-photo-icon.jpg b/src/components/intro/imgs/demos/large-photo-icon.jpg new file mode 100644 index 00000000..f418b268 Binary files /dev/null and b/src/components/intro/imgs/demos/large-photo-icon.jpg differ diff --git a/src/components/intro/imgs/demos/large-photo.jpg b/src/components/intro/imgs/demos/large-photo.jpg new file mode 100644 index 00000000..6f77519b Binary files /dev/null and b/src/components/intro/imgs/demos/large-photo.jpg differ diff --git a/src/components/intro/imgs/demos/logo-icon.png b/src/components/intro/imgs/demos/logo-icon.png new file mode 100644 index 00000000..44ff9216 Binary files /dev/null and b/src/components/intro/imgs/demos/logo-icon.png differ diff --git a/src/components/intro/imgs/logo.svg b/src/components/intro/imgs/logo.svg new file mode 100644 index 00000000..a989475d --- /dev/null +++ b/src/components/intro/imgs/logo.svg @@ -0,0 +1 @@ + diff --git a/src/components/intro/index.tsx b/src/components/intro/index.tsx index 5d46ff75..b239a306 100644 --- a/src/components/intro/index.tsx +++ b/src/components/intro/index.tsx @@ -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 { + 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 ( -
-

Drop, paste or select an image

- +
+
+
+
+ Squoosh +
+
+

+ Drag & drop or{' '} + + +

+

Or try one of these:

+
    + {demos.map((demo, i) => +
  • + +
  • , + )} +
+
+
); } diff --git a/src/components/intro/style.scss b/src/components/intro/style.scss index 3fa37a8c..1ab4d88a 100644 --- a/src/components/intro/style.scss +++ b/src/components/intro/style.scss @@ -1,22 +1,191 @@ -.welcome { - margin: auto; +@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'); +} + +@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; +} - h1 { - font-weight: inherit; - font-size: 150%; - text-align: center; - } +.logo-container { + position: relative; + padding-top: 100%; +} - input { - display: inline-block; - width: 16em; - padding: 10px; - margin: 0 auto; - -webkit-appearance: none; - border: 1px solid var(--button-fg); - background: rgba(var(--button-fg-color), 0.1); - border-radius: 3px; - cursor: pointer; +.logo-sizer { + width: 90%; + max-width: 480px; + margin: 0 auto; +} + +.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; + 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; + } } } diff --git a/src/lib/util.scss b/src/lib/util.scss new file mode 100644 index 00000000..f5b3241d --- /dev/null +++ b/src/lib/util.scss @@ -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; + } +} diff --git a/src/missing-types.d.ts b/src/missing-types.d.ts index ac8e3b63..b553e665 100644 --- a/src/missing-types.d.ts +++ b/src/missing-types.d.ts @@ -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; - - 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; +} diff --git a/src/style/index.scss b/src/style/index.scss index 1f0053f0..a22b7362 100644 --- a/src/style/index.scss +++ b/src/style/index.scss @@ -13,6 +13,8 @@ html, body { overflow: hidden; overscroll-behavior: none; contain: strict; + background: url('data:image/svg+xml,'); + background-size: 20px 20px; } :root { diff --git a/webpack.config.js b/webpack.config.js index 9833151e..0e2017bb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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' } ] },