feat: Installable PWA (#1882)
This commit is contained in:
parent
4b603460cb
commit
7e922d8716
|
@ -11,7 +11,7 @@ type Props = {|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
padding: ${(props) => (props.withStickyHeader ? "4px 20px" : "60px 20px")};
|
padding: ${(props) => (props.withStickyHeader ? "4px 12px" : "60px 12px")};
|
||||||
|
|
||||||
${breakpoint("tablet")`
|
${breakpoint("tablet")`
|
||||||
padding: ${(props) => (props.withStickyHeader ? "4px 60px" : "60px")};
|
padding: ${(props) => (props.withStickyHeader ? "4px 60px" : "60px")};
|
||||||
|
|
|
@ -163,8 +163,11 @@ const DocumentLink = styled(Link)`
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
min-width: 100%;
|
width: calc(100vw - 8px);
|
||||||
max-width: calc(100vw - 40px);
|
|
||||||
|
${breakpoint("tablet")`
|
||||||
|
width: auto;
|
||||||
|
`};
|
||||||
|
|
||||||
${Actions} {
|
${Actions} {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Helmet } from "react-helmet";
|
||||||
import { withTranslation, type TFunction } from "react-i18next";
|
import { withTranslation, type TFunction } from "react-i18next";
|
||||||
import keydown from "react-keydown";
|
import keydown from "react-keydown";
|
||||||
import { Switch, Route, Redirect } from "react-router-dom";
|
import { Switch, Route, Redirect } from "react-router-dom";
|
||||||
import styled, { withTheme } from "styled-components";
|
import styled from "styled-components";
|
||||||
import breakpoint from "styled-components-breakpoint";
|
import breakpoint from "styled-components-breakpoint";
|
||||||
import AuthStore from "stores/AuthStore";
|
import AuthStore from "stores/AuthStore";
|
||||||
import DocumentsStore from "stores/DocumentsStore";
|
import DocumentsStore from "stores/DocumentsStore";
|
||||||
|
@ -24,7 +24,6 @@ import Sidebar from "components/Sidebar";
|
||||||
import SettingsSidebar from "components/Sidebar/Settings";
|
import SettingsSidebar from "components/Sidebar/Settings";
|
||||||
import SkipNavContent from "components/SkipNavContent";
|
import SkipNavContent from "components/SkipNavContent";
|
||||||
import SkipNavLink from "components/SkipNavLink";
|
import SkipNavLink from "components/SkipNavLink";
|
||||||
import { type Theme } from "types";
|
|
||||||
import { meta } from "utils/keyboard";
|
import { meta } from "utils/keyboard";
|
||||||
import {
|
import {
|
||||||
homeUrl,
|
homeUrl,
|
||||||
|
@ -40,7 +39,6 @@ type Props = {
|
||||||
auth: AuthStore,
|
auth: AuthStore,
|
||||||
ui: UiStore,
|
ui: UiStore,
|
||||||
notifications?: React.Node,
|
notifications?: React.Node,
|
||||||
theme: Theme,
|
|
||||||
i18n: Object,
|
i18n: Object,
|
||||||
t: TFunction,
|
t: TFunction,
|
||||||
};
|
};
|
||||||
|
@ -51,24 +49,12 @@ class Layout extends React.Component<Props> {
|
||||||
@observable redirectTo: ?string;
|
@observable redirectTo: ?string;
|
||||||
@observable keyboardShortcutsOpen: boolean = false;
|
@observable keyboardShortcutsOpen: boolean = false;
|
||||||
|
|
||||||
constructor(props: Props) {
|
|
||||||
super();
|
|
||||||
this.updateBackground(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
this.updateBackground(this.props);
|
|
||||||
|
|
||||||
if (this.redirectTo) {
|
if (this.redirectTo) {
|
||||||
this.redirectTo = undefined;
|
this.redirectTo = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBackground(props: Props) {
|
|
||||||
// ensure the wider page color always matches the theme
|
|
||||||
window.document.body.style.background = props.theme.background;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keydown(`${meta}+.`)
|
@keydown(`${meta}+.`)
|
||||||
handleToggleSidebar() {
|
handleToggleSidebar() {
|
||||||
this.props.ui.toggleCollapsedSidebar();
|
this.props.ui.toggleCollapsedSidebar();
|
||||||
|
@ -212,5 +198,5 @@ const Content = styled(Flex)`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default withTranslation()<Layout>(
|
export default withTranslation()<Layout>(
|
||||||
inject("auth", "ui", "documents")(withTheme(Layout))
|
inject("auth", "ui", "documents")(Layout)
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from "react";
|
||||||
|
import { useTheme } from "styled-components";
|
||||||
|
import useStores from "hooks/useStores";
|
||||||
|
|
||||||
|
export default function PageTheme() {
|
||||||
|
const { ui } = useStores();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// wider page background beyond the React root
|
||||||
|
if (document.body) {
|
||||||
|
document.body.style.background = theme.background;
|
||||||
|
}
|
||||||
|
|
||||||
|
// theme-color adjusts the title bar color for desktop PWA
|
||||||
|
const themeElement = document.querySelector('meta[name="theme-color"]');
|
||||||
|
if (themeElement) {
|
||||||
|
themeElement.setAttribute("content", theme.background);
|
||||||
|
}
|
||||||
|
|
||||||
|
// status bar color for iOS PWA
|
||||||
|
const statusElement = document.querySelector(
|
||||||
|
'meta[name="apple-mobile-web-app-status-bar-style"]'
|
||||||
|
);
|
||||||
|
if (statusElement) {
|
||||||
|
statusElement.setAttribute(
|
||||||
|
"content",
|
||||||
|
ui.resolvedTheme === "dark" ? "black-translucent" : "default"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [theme, ui.resolvedTheme]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -46,6 +46,7 @@ export const ToggleButton = styled.button`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Positioner = styled.div`
|
export const Positioner = styled.div`
|
||||||
|
display: none;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -56,6 +57,10 @@ export const Positioner = styled.div`
|
||||||
&:hover ${ToggleButton}, &:focus-within ${ToggleButton} {
|
&:hover ${ToggleButton}, &:focus-within ${ToggleButton} {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${breakpoint("tablet")`
|
||||||
|
display: block;
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Toggle;
|
export default Toggle;
|
||||||
|
|
|
@ -33,7 +33,7 @@ const Sticky = styled.div`
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
background: ${(props) => props.theme.background};
|
background: ${(props) => props.theme.background};
|
||||||
transition: ${(props) => props.theme.backgroundTransition};
|
transition: ${(props) => props.theme.backgroundTransition};
|
||||||
z-index: ${(props) => props.theme.depths.stickyHeader};
|
z-index: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Subheading = ({ children, ...rest }: Props) => {
|
const Subheading = ({ children, ...rest }: Props) => {
|
||||||
|
|
|
@ -19,7 +19,7 @@ const Sticky = styled.div`
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
background: ${(props) => props.theme.background};
|
background: ${(props) => props.theme.background};
|
||||||
transition: ${(props) => props.theme.backgroundTransition};
|
transition: ${(props) => props.theme.backgroundTransition};
|
||||||
z-index: ${(props) => props.theme.depths.stickyHeader};
|
z-index: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Separator = styled.span`
|
export const Separator = styled.span`
|
||||||
|
|
19
app/index.js
19
app/index.js
|
@ -10,6 +10,7 @@ import { Router } from "react-router-dom";
|
||||||
import { initI18n } from "shared/i18n";
|
import { initI18n } from "shared/i18n";
|
||||||
import stores from "stores";
|
import stores from "stores";
|
||||||
import ErrorBoundary from "components/ErrorBoundary";
|
import ErrorBoundary from "components/ErrorBoundary";
|
||||||
|
import PageTheme from "components/PageTheme";
|
||||||
import ScrollToTop from "components/ScrollToTop";
|
import ScrollToTop from "components/ScrollToTop";
|
||||||
import Theme from "components/Theme";
|
import Theme from "components/Theme";
|
||||||
import Toasts from "components/Toasts";
|
import Toasts from "components/Toasts";
|
||||||
|
@ -19,13 +20,28 @@ import { initSentry } from "utils/sentry";
|
||||||
|
|
||||||
initI18n();
|
initI18n();
|
||||||
|
|
||||||
const element = document.getElementById("root");
|
const element = window.document.getElementById("root");
|
||||||
const history = createBrowserHistory();
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
if (env.SENTRY_DSN) {
|
if (env.SENTRY_DSN) {
|
||||||
initSentry(history);
|
initSentry(history);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("serviceWorker" in window.navigator) {
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
window.navigator.serviceWorker
|
||||||
|
.register("/static/service-worker.js", {
|
||||||
|
scope: "/",
|
||||||
|
})
|
||||||
|
.then((registration) => {
|
||||||
|
console.log("SW registered: ", registration);
|
||||||
|
})
|
||||||
|
.catch((registrationError) => {
|
||||||
|
console.log("SW registration failed: ", registrationError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
render(
|
render(
|
||||||
<Provider {...stores}>
|
<Provider {...stores}>
|
||||||
|
@ -34,6 +50,7 @@ if (element) {
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<>
|
<>
|
||||||
|
<PageTheme />
|
||||||
<ScrollToTop>
|
<ScrollToTop>
|
||||||
<Routes />
|
<Routes />
|
||||||
</ScrollToTop>
|
</ScrollToTop>
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { observer, inject } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import type { RouterHistory, Match } from "react-router-dom";
|
import type { RouterHistory, Match } from "react-router-dom";
|
||||||
import { withRouter } from "react-router-dom";
|
import { withRouter } from "react-router-dom";
|
||||||
import { withTheme } from "styled-components";
|
|
||||||
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
|
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
|
||||||
import DocumentsStore from "stores/DocumentsStore";
|
import DocumentsStore from "stores/DocumentsStore";
|
||||||
import PoliciesStore from "stores/PoliciesStore";
|
import PoliciesStore from "stores/PoliciesStore";
|
||||||
|
@ -22,7 +21,7 @@ import DocumentComponent from "./Document";
|
||||||
import HideSidebar from "./HideSidebar";
|
import HideSidebar from "./HideSidebar";
|
||||||
import Loading from "./Loading";
|
import Loading from "./Loading";
|
||||||
import SocketPresence from "./SocketPresence";
|
import SocketPresence from "./SocketPresence";
|
||||||
import { type LocationWithState, type Theme } from "types";
|
import { type LocationWithState } from "types";
|
||||||
import { NotFoundError, OfflineError } from "utils/errors";
|
import { NotFoundError, OfflineError } from "utils/errors";
|
||||||
import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers";
|
import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers";
|
||||||
import { isInternalUrl } from "utils/urls";
|
import { isInternalUrl } from "utils/urls";
|
||||||
|
@ -35,7 +34,6 @@ type Props = {|
|
||||||
policies: PoliciesStore,
|
policies: PoliciesStore,
|
||||||
revisions: RevisionsStore,
|
revisions: RevisionsStore,
|
||||||
ui: UiStore,
|
ui: UiStore,
|
||||||
theme: Theme,
|
|
||||||
history: RouterHistory,
|
history: RouterHistory,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
|
@ -49,7 +47,6 @@ class DataLoader extends React.Component<Props> {
|
||||||
const { documents, match } = this.props;
|
const { documents, match } = this.props;
|
||||||
this.document = documents.getByUrl(match.params.documentSlug);
|
this.document = documents.getByUrl(match.params.documentSlug);
|
||||||
this.loadDocument();
|
this.loadDocument();
|
||||||
this.updateBackground();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
@ -74,13 +71,6 @@ class DataLoader extends React.Component<Props> {
|
||||||
) {
|
) {
|
||||||
this.loadRevision();
|
this.loadRevision();
|
||||||
}
|
}
|
||||||
this.updateBackground();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBackground() {
|
|
||||||
// ensure the wider page color always matches the theme. This is to
|
|
||||||
// account for share links which don't sit in the wider Layout component
|
|
||||||
window.document.body.style.background = this.props.theme.background;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get isEditing() {
|
get isEditing() {
|
||||||
|
@ -266,5 +256,5 @@ export default withRouter(
|
||||||
"revisions",
|
"revisions",
|
||||||
"policies",
|
"policies",
|
||||||
"shares"
|
"shares"
|
||||||
)(withTheme(DataLoader))
|
)(DataLoader)
|
||||||
);
|
);
|
||||||
|
|
|
@ -480,7 +480,7 @@ const ReferencesWrapper = styled("div")`
|
||||||
const MaxWidth = styled(Flex)`
|
const MaxWidth = styled(Flex)`
|
||||||
${(props) =>
|
${(props) =>
|
||||||
props.archived && `* { color: ${props.theme.textSecondary} !important; } `};
|
props.archived && `* { color: ${props.theme.textSecondary} !important; } `};
|
||||||
padding: 0 16px;
|
padding: 0 12px;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
|
|
@ -206,11 +206,13 @@
|
||||||
"url-loader": "^0.6.2",
|
"url-loader": "^0.6.2",
|
||||||
"webpack": "4.44.1",
|
"webpack": "4.44.1",
|
||||||
"webpack-cli": "^3.3.12",
|
"webpack-cli": "^3.3.12",
|
||||||
"webpack-manifest-plugin": "^3.0.0"
|
"webpack-manifest-plugin": "^3.0.0",
|
||||||
|
"webpack-pwa-manifest": "^4.3.0",
|
||||||
|
"workbox-webpack-plugin": "^6.1.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"dot-prop": "^5.2.0",
|
"dot-prop": "^5.2.0",
|
||||||
"js-yaml": "^3.13.1"
|
"js-yaml": "^3.13.1"
|
||||||
},
|
},
|
||||||
"version": "0.52.0"
|
"version": "0.52.0"
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 48 KiB |
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"short_name": "Outline",
|
|
||||||
"name": "Outline",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/icon-192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/icon-512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": "/home?source=pwa",
|
|
||||||
"background_color": "#FFFFFF",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#FFFFFF"
|
|
||||||
}
|
|
|
@ -67,6 +67,8 @@ router.get("/_health", (ctx) => (ctx.body = "OK"));
|
||||||
if (process.env.NODE_ENV === "production") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
router.get("/static/*", async (ctx) => {
|
router.get("/static/*", async (ctx) => {
|
||||||
ctx.set({
|
ctx.set({
|
||||||
|
"Service-Worker-Allowed": "/",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
"Cache-Control": `max-age=${356 * 24 * 60 * 60}`,
|
"Cache-Control": `max-age=${356 * 24 * 60 * 60}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,11 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Outline</title>
|
<title>Outline</title>
|
||||||
|
<meta name="theme-color" content="#FFF" />
|
||||||
<meta name="slack-app-id" content="//inject-slack-app-id//" />
|
<meta name="slack-app-id" content="//inject-slack-app-id//" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<meta name="description" content="A modern team knowledge base for your internal documentation, product specs, support answers, meeting notes, onboarding, & more…">
|
<meta name="description" content="A modern team knowledge base for your internal documentation, product specs, support answers, meeting notes, onboarding, & more…">
|
||||||
//inject-prefetch//
|
//inject-prefetch//
|
||||||
<link
|
<link
|
||||||
|
@ -12,7 +15,12 @@
|
||||||
href="/favicon-32.png"
|
href="/favicon-32.png"
|
||||||
sizes="32x32"
|
sizes="32x32"
|
||||||
/>
|
/>
|
||||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
type="image/png"
|
||||||
|
href="/apple-touch-icon.png"
|
||||||
|
sizes="192x192"
|
||||||
|
/>
|
||||||
<link
|
<link
|
||||||
rel="search"
|
rel="search"
|
||||||
type="application/opensearchdescription+xml"
|
type="application/opensearchdescription+xml"
|
||||||
|
@ -46,7 +54,9 @@
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
if (window.localStorage && window.localStorage.getItem("theme") === "dark") {
|
if (window.localStorage && window.localStorage.getItem("theme") === "dark") {
|
||||||
window.document.querySelector("#root").style.background = "#111319";
|
var color = "#111319";
|
||||||
|
document.querySelector("#root").style.background = color;
|
||||||
|
document.querySelector('meta[name="theme-color"]').setAttribute("content", color);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -37,6 +37,21 @@ export default createGlobalStyle`
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: ${(props) =>
|
||||||
|
props.theme.breakpoints.tablet}px) and (display-mode: standalone) {
|
||||||
|
body:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: ${(props) => props.theme.divider};
|
||||||
|
z-index: ${(props) => props.theme.depths.pwaSeparator};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: ${(props) => props.theme.link};
|
color: ${(props) => props.theme.link};
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
@ -108,13 +108,13 @@ export const base = {
|
||||||
|
|
||||||
depths: {
|
depths: {
|
||||||
sidebar: 1000,
|
sidebar: 1000,
|
||||||
stickyHeader: 1500,
|
|
||||||
modalOverlay: 2000,
|
modalOverlay: 2000,
|
||||||
modal: 3000,
|
modal: 3000,
|
||||||
menu: 4000,
|
menu: 4000,
|
||||||
toasts: 5000,
|
toasts: 5000,
|
||||||
loadingIndicatorBar: 6000,
|
loadingIndicatorBar: 6000,
|
||||||
popover: 9000,
|
popover: 9000,
|
||||||
|
pwaSeparator: 10000,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ const webpack = require('webpack');
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const { RelativeCiAgentWebpackPlugin } = require('@relative-ci/agent');
|
const { RelativeCiAgentWebpackPlugin } = require('@relative-ci/agent');
|
||||||
const pkg = require("rich-markdown-editor/package.json");
|
const pkg = require("rich-markdown-editor/package.json");
|
||||||
|
const WebpackPwaManifest = require("webpack-pwa-manifest");
|
||||||
|
const WorkboxPlugin = require("workbox-webpack-plugin");
|
||||||
|
|
||||||
require('dotenv').config({ silent: true });
|
require('dotenv').config({ silent: true });
|
||||||
|
|
||||||
|
@ -59,6 +61,30 @@ module.exports = {
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: 'server/static/index.html',
|
template: 'server/static/index.html',
|
||||||
}),
|
}),
|
||||||
|
new WebpackPwaManifest({
|
||||||
|
name: "Outline",
|
||||||
|
short_name: "Outline",
|
||||||
|
background_color: "#fff",
|
||||||
|
theme_color: "#fff",
|
||||||
|
start_url: process.env.URL,
|
||||||
|
display: "standalone",
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: path.resolve("public/icon-512.png"),
|
||||||
|
// For Chrome, you must provide at least a 192x192 pixel icon, and a 512x512 pixel icon.
|
||||||
|
// If only those two icon sizes are provided, Chrome will automatically scale the icons
|
||||||
|
// to fit the device. If you'd prefer to scale your own icons, and adjust them for
|
||||||
|
// pixel-perfection, provide icons in increments of 48dp.
|
||||||
|
sizes: [512, 192],
|
||||||
|
purpose: "any maskable",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
new WorkboxPlugin.GenerateSW({
|
||||||
|
clientsClaim: true,
|
||||||
|
skipWaiting: true,
|
||||||
|
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024, // For large bundles
|
||||||
|
}),
|
||||||
new RelativeCiAgentWebpackPlugin(),
|
new RelativeCiAgentWebpackPlugin(),
|
||||||
],
|
],
|
||||||
stats: {
|
stats: {
|
||||||
|
|
Reference in New Issue