feat: make Personal Workspace installable as a PWA

This commit is contained in:
2026-10-08 00:47:03 +09:00
parent aabeb33f41
commit 67afc7bf15
12 changed files with 280 additions and 5 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect width="512" height="512" fill="#255f6e"/>
<g fill="#fafafa">
<rect x="144" y="144" width="80" height="224" rx="16"/>
<rect x="248" y="144" width="120" height="96" rx="16"/>
<rect x="248" y="264" width="120" height="104" rx="16"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 329 B

+22
View File
@@ -0,0 +1,22 @@
{
"id": "/",
"name": "Personal Workspace",
"short_name": "Workspace",
"description": "Your day, messages, notes, reading, and research in one place.",
"lang": "en",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#171717",
"theme_color": "#171717",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
{
"src": "/icons/maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
+56
View File
@@ -0,0 +1,56 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#171717" />
<title>Offline · Personal Workspace</title>
<style>
:root {
color-scheme: dark;
font-family: system-ui, sans-serif;
background: #171717;
color: #fafafa;
}
body {
margin: 0;
min-height: 100svh;
display: grid;
place-items: center;
}
main {
max-width: 28rem;
padding: 2rem;
}
h1 {
font-size: 1.75rem;
}
p {
color: #b4b4b4;
line-height: 1.6;
}
button {
margin-top: 1rem;
border: 0;
border-radius: 0.625rem;
padding: 0.75rem 1.25rem;
background: #fafafa;
color: #171717;
font: inherit;
cursor: pointer;
}
button:focus-visible {
outline: 2px solid #fafafa;
outline-offset: 4px;
}
</style>
</head>
<body>
<main>
<p>Personal Workspace</p>
<h1>You’re offline</h1>
<p>Connect to the internet and try again to open your workspace.</p>
<button type="button" onclick="window.location.reload()">Try again</button>
</main>
</body>
</html>
+47
View File
@@ -0,0 +1,47 @@
// Only this generic page is cached. Authenticated HTML, API responses, and
// mutations always use the network. Bump the version when offline.html changes.
const CACHE = "workspace-offline-v1";
const OFFLINE_URL = "/offline.html";
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE).then((cache) => cache.add(new Request(OFFLINE_URL, { cache: "reload" }))),
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const keys = await caches.keys();
await Promise.all(
keys
.filter((key) => key.startsWith("workspace-offline-") && key !== CACHE)
.map((key) => caches.delete(key)),
);
await self.clients.claim();
})(),
);
});
self.addEventListener("fetch", (event) => {
const { request } = event;
const url = new URL(request.url);
if (
request.method !== "GET" ||
request.mode !== "navigate" ||
url.origin !== self.location.origin ||
url.pathname.startsWith("/api/") ||
url.pathname.startsWith("/_serverFn/") ||
url.pathname.startsWith("/oauth/") ||
url.pathname === "/mcp" ||
url.pathname === "/health"
) {
return;
}
event.respondWith(
fetch(request).catch(async () => {
const cache = await caches.open(CACHE);
return (await cache.match(OFFLINE_URL)) ?? Response.error();
}),
);
});