Add fullscreen PWA support

This commit is contained in:
2026-05-30 00:32:57 -07:00
parent a6c2ec664b
commit 39014eee18
15 changed files with 114 additions and 8 deletions

View File

@@ -3,12 +3,18 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content" />
<meta name="description" content="Sybil chat and search workspace" />
<meta name="application-name" content="Sybil" />
<meta name="theme-color" content="#0f172a" /> <meta name="theme-color" content="#0f172a" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Sybil" /> <meta name="apple-mobile-web-app-title" content="Sybil" />
<meta name="format-detection" content="telephone=no" />
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png" />
<link rel="search" type="application/opensearchdescription+xml" title="Sybil Search" href="/opensearch.xml" /> <link rel="search" type="application/opensearchdescription+xml" title="Sybil Search" href="/opensearch.xml" />
<title>Sybil</title> <title>Sybil</title>
</head> </head>

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

View File

@@ -1,9 +1,32 @@
{ {
"id": "/",
"name": "Sybil", "name": "Sybil",
"short_name": "Sybil", "short_name": "Sybil",
"description": "Sybil chat and search workspace",
"start_url": "/", "start_url": "/",
"scope": "/", "scope": "/",
"display": "standalone", "display": "fullscreen",
"background_color": "#ffffff", "display_override": ["fullscreen", "standalone"],
"theme_color": "#0f172a" "background_color": "#0b0718",
"theme_color": "#0f172a",
"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/icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
} }

12
web/public/sw.js Normal file
View File

@@ -0,0 +1,12 @@
self.addEventListener("install", () => {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener("fetch", (event) => {
if (event.request.mode !== "navigate") return;
event.respondWith(fetch(event.request));
});

View File

@@ -2617,7 +2617,7 @@ export default function App() {
} }
return ( return (
<div className="app-grid-surface h-full p-0 md:p-2"> <div className="app-grid-surface app-safe-frame h-full">
<div className="flex h-full w-full overflow-hidden bg-transparent md:gap-2"> <div className="flex h-full w-full overflow-hidden bg-transparent md:gap-2">
{isMobileSidebarOpen ? ( {isMobileSidebarOpen ? (
<button <button

View File

@@ -12,7 +12,7 @@ type Props = {
export function AuthScreen({ authTokenInput, setAuthTokenInput, isSigningIn, authError, onSignIn }: Props) { export function AuthScreen({ authTokenInput, setAuthTokenInput, isSigningIn, authError, onSignIn }: Props) {
return ( return (
<div className="app-grid-surface flex h-full items-center justify-center p-4"> <div className="app-grid-surface app-safe-pad flex h-full items-center justify-center">
<div className="glass-panel w-full max-w-md rounded-2xl border border-violet-300/18 p-6"> <div className="glass-panel w-full max-w-md rounded-2xl border border-violet-300/18 p-6">
<div className="mb-6"> <div className="mb-6">
<div className="sybil-wordmark bg-[linear-gradient(90deg,#ff8df8,#9a6dff_54%,#67dfff)] bg-clip-text text-3xl text-transparent"> <div className="sybil-wordmark bg-[linear-gradient(90deg,#ff8df8,#9a6dff_54%,#67dfff)] bg-clip-text text-3xl text-transparent">

View File

@@ -14,6 +14,10 @@
:root { :root {
color-scheme: dark; color-scheme: dark;
--safe-area-top: env(safe-area-inset-top, 0px);
--safe-area-right: env(safe-area-inset-right, 0px);
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
--safe-area-left: env(safe-area-inset-left, 0px);
--background: 235 45% 4%; --background: 235 45% 4%;
--foreground: 258 36% 96%; --foreground: 258 36% 96%;
--muted: 246 30% 13%; --muted: 246 30% 13%;
@@ -40,6 +44,15 @@ html,
body, body,
#app { #app {
height: 100%; height: 100%;
width: 100%;
}
@supports (height: 100dvh) {
html,
body,
#app {
height: 100dvh;
}
} }
body { body {
@@ -49,6 +62,8 @@ body {
linear-gradient(90deg, hsl(187 92% 49% / 0.08), transparent 24%, hsl(264 92% 59% / 0.12) 74%, transparent), linear-gradient(90deg, hsl(187 92% 49% / 0.08), transparent 24%, hsl(264 92% 59% / 0.12) 74%, transparent),
linear-gradient(180deg, hsl(250 60% 16% / 0.68), hsl(235 45% 4%) 48%, hsl(235 54% 3%)); linear-gradient(180deg, hsl(250 60% 16% / 0.68), hsl(235 45% 4%) 48%, hsl(235 54% 3%));
font-family: "Inter", "Avenir Next", "Segoe UI", sans-serif; font-family: "Inter", "Avenir Next", "Segoe UI", sans-serif;
overflow: hidden;
overscroll-behavior: none;
} }
button, button,
@@ -78,6 +93,44 @@ textarea {
background-size: 48px 48px; background-size: 48px 48px;
} }
.app-safe-frame {
padding: var(--safe-area-top) var(--safe-area-right) var(--safe-area-bottom) var(--safe-area-left);
}
.app-safe-pad {
padding:
max(1rem, var(--safe-area-top))
max(1rem, var(--safe-area-right))
max(1rem, var(--safe-area-bottom))
max(1rem, var(--safe-area-left));
}
.app-search-safe-pad {
padding:
max(1.5rem, var(--safe-area-top))
max(0.75rem, var(--safe-area-right))
max(1.5rem, var(--safe-area-bottom))
max(0.75rem, var(--safe-area-left));
}
@media (min-width: 768px) {
.app-safe-frame {
padding:
max(0.5rem, var(--safe-area-top))
max(0.5rem, var(--safe-area-right))
max(0.5rem, var(--safe-area-bottom))
max(0.5rem, var(--safe-area-left));
}
.app-search-safe-pad {
padding:
max(1.5rem, var(--safe-area-top))
max(1.5rem, var(--safe-area-right))
max(1.5rem, var(--safe-area-bottom))
max(1.5rem, var(--safe-area-left));
}
}
.glass-panel { .glass-panel {
background: background:
linear-gradient(180deg, hsl(243 42% 12% / 0.88), hsl(236 48% 5% / 0.92)), linear-gradient(180deg, hsl(243 42% 12% / 0.88), hsl(236 48% 5% / 0.92)),

View File

@@ -1,5 +1,8 @@
import { render } from "preact"; import { render } from "preact";
import { RootRouter } from "@/root-router"; import { RootRouter } from "@/root-router";
import { registerServiceWorker } from "@/pwa";
import "./index.css"; import "./index.css";
registerServiceWorker();
render(<RootRouter />, document.getElementById("app")!); render(<RootRouter />, document.getElementById("app")!);

View File

@@ -252,7 +252,7 @@ export default function SearchRoutePage() {
} }
return ( return (
<div className="h-full overflow-y-auto px-3 py-6 md:px-6"> <div className="app-search-safe-pad h-full overflow-y-auto">
<div className="mx-auto w-full max-w-4xl space-y-5"> <div className="mx-auto w-full max-w-4xl space-y-5">
<form <form
className="flex items-center gap-2 rounded-xl border bg-background p-2 shadow-sm" className="flex items-center gap-2 rounded-xl border bg-background p-2 shadow-sm"

9
web/src/pwa.ts Normal file
View File

@@ -0,0 +1,9 @@
export function registerServiceWorker() {
if (!import.meta.env.PROD || !("serviceWorker" in navigator)) return;
window.addEventListener("load", () => {
void navigator.serviceWorker.register("/sw.js").catch((error: unknown) => {
console.warn("Sybil service worker registration failed", error);
});
});
}

View File

@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/root-router.tsx","./src/vite-env.d.ts","./src/components/sybil-character.tsx","./src/components/auth/auth-screen.tsx","./src/components/chat/chat-attachment-list.tsx","./src/components/chat/chat-messages-panel.tsx","./src/components/markdown/markdown-content.tsx","./src/components/search/search-results-panel.tsx","./src/components/ui/button.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/separator.tsx","./src/components/ui/textarea.tsx","./src/hooks/use-session-auth.ts","./src/lib/api.ts","./src/lib/utils.ts","./src/pages/search-route-page.tsx"],"version":"5.9.3"} {"root":["./src/App.tsx","./src/main.tsx","./src/pwa.ts","./src/root-router.tsx","./src/vite-env.d.ts","./src/components/sybil-character.tsx","./src/components/auth/auth-screen.tsx","./src/components/chat/chat-attachment-list.tsx","./src/components/chat/chat-messages-panel.tsx","./src/components/markdown/markdown-content.tsx","./src/components/search/search-results-panel.tsx","./src/components/ui/button.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/separator.tsx","./src/components/ui/textarea.tsx","./src/hooks/use-session-auth.ts","./src/lib/api.ts","./src/lib/utils.ts","./src/pages/search-route-page.tsx"],"version":"5.9.3"}