Add dashboard PWA shell and status UX
This commit is contained in:
BIN
apps/dashboard/public/icons/icon-192.png
Normal file
BIN
apps/dashboard/public/icons/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 861 B |
8
apps/dashboard/public/icons/icon-192.svg
Normal file
8
apps/dashboard/public/icons/icon-192.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192" role="img" aria-label="EJClaw">
|
||||
<rect width="192" height="192" rx="46" fill="#2b3726" />
|
||||
<path d="M39 122c18 21 47 29 81 17 18-6 30-18 36-35" fill="none" stroke="#f7edda" stroke-width="14" stroke-linecap="round" />
|
||||
<path d="M50 78c16-24 45-36 76-25 13 5 24 13 32 25" fill="none" stroke="#bf5f2c" stroke-width="14" stroke-linecap="round" />
|
||||
<circle cx="74" cy="94" r="11" fill="#f7edda" />
|
||||
<circle cx="118" cy="94" r="11" fill="#f7edda" />
|
||||
<path d="M88 126h21" stroke="#f7edda" stroke-width="10" stroke-linecap="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 627 B |
BIN
apps/dashboard/public/icons/icon-512.png
Normal file
BIN
apps/dashboard/public/icons/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
8
apps/dashboard/public/icons/icon-512.svg
Normal file
8
apps/dashboard/public/icons/icon-512.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-label="EJClaw">
|
||||
<rect width="512" height="512" rx="124" fill="#2b3726" />
|
||||
<path d="M104 325c48 58 126 78 217 47 48-16 80-49 96-93" fill="none" stroke="#f7edda" stroke-width="38" stroke-linecap="round" />
|
||||
<path d="M134 208c43-64 120-96 203-66 36 13 64 35 86 66" fill="none" stroke="#bf5f2c" stroke-width="38" stroke-linecap="round" />
|
||||
<circle cx="198" cy="250" r="30" fill="#f7edda" />
|
||||
<circle cx="314" cy="250" r="30" fill="#f7edda" />
|
||||
<path d="M235 336h56" stroke="#f7edda" stroke-width="28" stroke-linecap="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 641 B |
27
apps/dashboard/public/manifest.webmanifest
Normal file
27
apps/dashboard/public/manifest.webmanifest
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "EJClaw Ops",
|
||||
"short_name": "EJClaw",
|
||||
"description": "EJClaw operations console",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"display_override": ["window-controls-overlay", "standalone", "browser"],
|
||||
"background_color": "#f3efe4",
|
||||
"theme_color": "#2b3726",
|
||||
"orientation": "portrait-primary",
|
||||
"categories": ["productivity", "utilities"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
87
apps/dashboard/public/offline.html
Normal file
87
apps/dashboard/public/offline.html
Normal file
@@ -0,0 +1,87 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
||||
/>
|
||||
<meta name="theme-color" content="#2b3726" />
|
||||
<title>EJClaw Offline</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
font-family:
|
||||
Avenir Next,
|
||||
Segoe UI Variable,
|
||||
Noto Sans KR,
|
||||
ui-sans-serif,
|
||||
sans-serif;
|
||||
background: #f3efe4;
|
||||
color: #1c211c;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
place-items: center;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 20% 18%,
|
||||
rgba(191, 95, 44, 0.2),
|
||||
transparent 26rem
|
||||
),
|
||||
linear-gradient(135deg, #f4ead5, #edf1df);
|
||||
}
|
||||
|
||||
main {
|
||||
width: min(420px, calc(100% - 32px));
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(43, 55, 38, 0.16);
|
||||
border-radius: 28px;
|
||||
background: rgba(255, 250, 240, 0.86);
|
||||
box-shadow: 0 24px 70px rgba(44, 39, 25, 0.16);
|
||||
}
|
||||
|
||||
span {
|
||||
color: #8f351b;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 8px 0;
|
||||
font-size: 30px;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 18px;
|
||||
color: #6d735f;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 44px;
|
||||
padding: 0 18px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
color: #fffaf0;
|
||||
background: linear-gradient(135deg, #bf5f2c, #8f351b);
|
||||
font: inherit;
|
||||
font-weight: 900;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<span>EJClaw</span>
|
||||
<h1>Offline</h1>
|
||||
<p>네트워크가 복구되면 대시보드를 다시 불러옵니다.</p>
|
||||
<button type="button" onclick="window.location.reload()">Retry</button>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
78
apps/dashboard/public/sw.js
Normal file
78
apps/dashboard/public/sw.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const CACHE_NAME = 'ejclaw-dashboard-v1';
|
||||
const PRECACHE_URLS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/offline.html',
|
||||
'/manifest.webmanifest',
|
||||
'/icons/icon-192.png',
|
||||
'/icons/icon-512.png',
|
||||
'/icons/icon-192.svg',
|
||||
'/icons/icon-512.svg',
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(CACHE_NAME)
|
||||
.then((cache) => cache.addAll(PRECACHE_URLS))
|
||||
.then(() => self.skipWaiting()),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.keys()
|
||||
.then((keys) =>
|
||||
Promise.all(
|
||||
keys
|
||||
.filter((key) => key !== CACHE_NAME)
|
||||
.map((key) => caches.delete(key)),
|
||||
),
|
||||
)
|
||||
.then(() => self.clients.claim()),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (request.method !== 'GET' || url.origin !== self.location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
event.respondWith(fetch(request));
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.mode === 'navigate') {
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
const copy = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => cache.put('/', copy));
|
||||
return response;
|
||||
})
|
||||
.catch(() =>
|
||||
caches
|
||||
.match('/')
|
||||
.then((cached) => cached || caches.match('/offline.html')),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.match(request).then((cached) => {
|
||||
if (cached) return cached;
|
||||
return fetch(request).then((response) => {
|
||||
if (!response.ok) return response;
|
||||
const copy = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy));
|
||||
return response;
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user