feat(pwa): favicon, maskable icon, splash screens, runtime cache, safe-area insets
- Add favicon.ico, favicon-32x32.png, favicon-16x16.png - Add dedicated maskable-512x512.png with safe-zone padding for Android adaptive icons - Add iOS splash screens for 5 common device sizes (excluded from SW precache) - Add NetworkFirst runtime caching for stable API routes (excludes volatile endpoints) - Add safe-top inset to all view headers for PWA standalone mode - Bump version to 0.2.2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@@ -7,6 +7,15 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="ClawTap" />
|
<meta name="apple-mobile-web-app-title" content="ClawTap" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="48x48" />
|
||||||
|
<!-- iOS Splash Screens -->
|
||||||
|
<link rel="apple-touch-startup-image" href="/splash/apple-splash-1290x2796.png" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3)" />
|
||||||
|
<link rel="apple-touch-startup-image" href="/splash/apple-splash-1179x2556.png" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3)" />
|
||||||
|
<link rel="apple-touch-startup-image" href="/splash/apple-splash-1170x2532.png" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3)" />
|
||||||
|
<link rel="apple-touch-startup-image" href="/splash/apple-splash-750x1334.png" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" />
|
||||||
|
<link rel="apple-touch-startup-image" href="/splash/apple-splash-2048x2732.png" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)" />
|
||||||
<meta name="theme-color" content="#09090b" />
|
<meta name="theme-color" content="#09090b" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kuannnn/clawtap",
|
"name": "@kuannnn/clawtap",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"description": "Mobile UI for AI coding assistants. Real-time sync with Claude Code, Codex CLI, and Gemini CLI via tmux.",
|
"description": "Mobile UI for AI coding assistants. Real-time sync with Claude Code, Codex CLI, and Gemini CLI via tmux.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
|
After Width: | Height: | Size: 404 B |
|
After Width: | Height: | Size: 671 B |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
@@ -36,7 +36,7 @@ export function AdapterSettingsSection({ adapter, onBack }: { adapter: string; o
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-bg">
|
<div className="flex flex-col h-full bg-bg">
|
||||||
<div className="flex items-center px-4 py-3 border-b border-border gap-2">
|
<div className="flex items-center px-4 py-3 border-b border-border gap-2 safe-top">
|
||||||
<button onClick={onBack} className="text-text-dim hover:text-text"><ChevronLeft className="w-5 h-5" /></button>
|
<button onClick={onBack} className="text-text-dim hover:text-text"><ChevronLeft className="w-5 h-5" /></button>
|
||||||
<AdapterIcon adapterId={adapter} size={20} />
|
<AdapterIcon adapterId={adapter} size={20} />
|
||||||
<span className="font-medium text-text font-mono tracking-wide">{brand.displayName} Settings</span>
|
<span className="font-medium text-text font-mono tracking-wide">{brand.displayName} Settings</span>
|
||||||
|
|||||||
@@ -477,7 +477,7 @@ export function ChatView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-dvh bg-bg relative overflow-hidden">
|
<div className="flex flex-col h-dvh bg-bg relative overflow-hidden safe-top">
|
||||||
{/* Header — auto-hides when scrolling up to view history */}
|
{/* Header — auto-hides when scrolling up to view history */}
|
||||||
<div className={`flex items-center gap-2 px-4 py-3 border-b border-border shrink-0 transition-all duration-200 ${headerHidden ? 'max-h-0 py-0 overflow-hidden opacity-0 border-b-0' : 'max-h-16 opacity-100'}`}>
|
<div className={`flex items-center gap-2 px-4 py-3 border-b border-border shrink-0 transition-all duration-200 ${headerHidden ? 'max-h-0 py-0 overflow-hidden opacity-0 border-b-0' : 'max-h-16 opacity-100'}`}>
|
||||||
<Button variant="ghost" size="icon" onClick={onBack}>
|
<Button variant="ghost" size="icon" onClick={onBack}>
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export function NewChatView({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen bg-bg">
|
<div className="flex flex-col h-screen bg-bg">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-2 px-4 py-3 border-b border-border shrink-0">
|
<div className="flex items-center gap-2 px-4 py-3 border-b border-border shrink-0 safe-top">
|
||||||
<Button variant="ghost" size="icon" onClick={onBack}>
|
<Button variant="ghost" size="icon" onClick={onBack}>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function SavedInstructionsView({ onBack }: { onBack: () => void }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-bg">
|
<div className="flex flex-col h-full bg-bg">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center px-4 py-3 border-b border-border">
|
<div className="flex items-center px-4 py-3 border-b border-border safe-top">
|
||||||
<button
|
<button
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="text-text-dim hover:text-text mr-2"
|
className="text-text-dim hover:text-text mr-2"
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export function SessionsView({
|
|||||||
// --- Projects list (default view) ---
|
// --- Projects list (default view) ---
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-bg flex flex-col">
|
<div className="min-h-screen bg-bg flex flex-col">
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0 safe-top">
|
||||||
<span className="flex items-center gap-1.5 text-lg font-medium text-text font-mono tracking-wider">
|
<span className="flex items-center gap-1.5 text-lg font-medium text-text font-mono tracking-wider">
|
||||||
<svg width="20" height="15" viewBox="0 0 8 6" style={{ imageRendering: 'pixelated' }} className="text-accent">
|
<svg width="20" height="15" viewBox="0 0 8 6" style={{ imageRendering: 'pixelated' }} className="text-accent">
|
||||||
<rect x="4" y="0" width="4" height="1" fill="currentColor"/>
|
<rect x="4" y="0" width="4" height="1" fill="currentColor"/>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function SettingsView({ onBack }: { onBack: () => void }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-bg">
|
<div className="flex flex-col h-full bg-bg">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center px-4 py-3 border-b border-border">
|
<div className="flex items-center px-4 py-3 border-b border-border safe-top">
|
||||||
<button onClick={onBack} className="text-text-dim hover:text-text mr-2"><ChevronLeft className="w-5 h-5" /></button>
|
<button onClick={onBack} className="text-text-dim hover:text-text mr-2"><ChevronLeft className="w-5 h-5" /></button>
|
||||||
<span className="text-lg font-medium text-text font-mono tracking-wide">Settings</span>
|
<span className="text-lg font-medium text-text font-mono tracking-wide">Settings</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,30 @@
|
|||||||
/// <reference lib="webworker" />
|
/// <reference lib="webworker" />
|
||||||
import { precacheAndRoute } from 'workbox-precaching';
|
import { precacheAndRoute } from 'workbox-precaching';
|
||||||
|
import { registerRoute } from 'workbox-routing';
|
||||||
|
import { NetworkFirst } from 'workbox-strategies';
|
||||||
|
|
||||||
declare const self: ServiceWorkerGlobalScope;
|
declare const self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
// Precache static assets (injected by vite-plugin-pwa at build time)
|
// Precache static assets (injected by vite-plugin-pwa at build time)
|
||||||
precacheAndRoute(self.__WB_MANIFEST);
|
precacheAndRoute(self.__WB_MANIFEST);
|
||||||
|
|
||||||
|
// Cache stable API responses — show last-known data when offline.
|
||||||
|
// Exclude volatile real-time endpoints (messages, active sessions, reviews).
|
||||||
|
registerRoute(
|
||||||
|
({ url }) => {
|
||||||
|
const p = url.pathname;
|
||||||
|
if (!p.startsWith('/api/')) return false;
|
||||||
|
if (p.includes('/messages')) return false;
|
||||||
|
if (p.startsWith('/api/active-sessions')) return false;
|
||||||
|
if (p.startsWith('/api/reviews')) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
new NetworkFirst({
|
||||||
|
cacheName: 'api-cache',
|
||||||
|
networkTimeoutSeconds: 5,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Push notification handler — server already filters by clientCount,
|
// Push notification handler — server already filters by clientCount,
|
||||||
// so we always show the notification if one is received.
|
// so we always show the notification if one is received.
|
||||||
self.addEventListener('push', (event) => {
|
self.addEventListener('push', (event) => {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default defineConfig({
|
|||||||
icons: [
|
icons: [
|
||||||
{ src: '/pwa-192x192.png', sizes: '192x192', type: 'image/png' },
|
{ src: '/pwa-192x192.png', sizes: '192x192', type: 'image/png' },
|
||||||
{ src: '/pwa-512x512.png', sizes: '512x512', type: 'image/png' },
|
{ src: '/pwa-512x512.png', sizes: '512x512', type: 'image/png' },
|
||||||
{ src: '/pwa-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
|
{ src: '/maskable-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
|
||||||
],
|
],
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{
|
{
|
||||||
@@ -39,7 +39,7 @@ export default defineConfig({
|
|||||||
categories: ['developer-tools', 'productivity'],
|
categories: ['developer-tools', 'productivity'],
|
||||||
},
|
},
|
||||||
injectManifest: {
|
injectManifest: {
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
|
globPatterns: ['**/*.{js,css,html,ico,svg,woff2}', '*.png', 'mascot/*.png'],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||