improve PWA feel on iOS: offline fallback, overscroll, tap feedback, dark status bar, apple touch icon
Add offline.html served by the service worker when a never-visited route is requested without network. Disable rubber-band overscroll bounce and gray tap highlights so standalone mode feels less like a webview. Split theme-color by prefers-color-scheme so the iOS status bar matches dark mode, and add a proper 180x180 apple-touch-icon instead of downscaling the 192px icon.
This commit is contained in:
@@ -46,6 +46,7 @@
|
|||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
[x-cloak] {
|
[x-cloak] {
|
||||||
@@ -56,6 +57,7 @@ html,
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
overscroll-behavior-y: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -239,6 +241,7 @@ p {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-tab-bar .tab-item svg {
|
.bottom-tab-bar .tab-item svg {
|
||||||
@@ -277,6 +280,7 @@ p {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
transition: background-color 0.15s ease, opacity 0.15s ease;
|
transition: background-color 0.15s ease, opacity 0.15s ease;
|
||||||
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@@ -452,6 +456,7 @@ label {
|
|||||||
gap: 8px var(--space-2);
|
gap: 8px var(--space-2);
|
||||||
padding: 12px 4px;
|
padding: 12px 4px;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
.habit-item:last-child {
|
.habit-item:last-child {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 857 B |
@@ -0,0 +1,23 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<title>오프라인 - 습관 트래커</title>
|
||||||
|
<link rel="icon" href="/static/icons/icon-192.png" />
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-shell" style="display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: var(--space-3);">
|
||||||
|
<div class="card" style="text-align: center; max-width: 360px;">
|
||||||
|
<p style="font-size: 15px; color: var(--color-text); margin: 0 0 var(--space-2);">
|
||||||
|
오프라인 상태예요. 이 화면은 아직 캐시에 저장되지 않았어요.
|
||||||
|
</p>
|
||||||
|
<p style="font-size: 13px; color: var(--color-text-muted); margin: 0 0 var(--space-3);">
|
||||||
|
네트워크 연결을 확인한 뒤 다시 시도해주세요.
|
||||||
|
</p>
|
||||||
|
<button type="button" class="btn btn-primary btn-block" onclick="location.reload()">다시 시도</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_NAME = "habit-tracker-v3";
|
const CACHE_NAME = "habit-tracker-v4";
|
||||||
const APP_SHELL = [
|
const APP_SHELL = [
|
||||||
"/static/css/style.css",
|
"/static/css/style.css",
|
||||||
"/static/js/app.js",
|
"/static/js/app.js",
|
||||||
@@ -9,7 +9,9 @@ const APP_SHELL = [
|
|||||||
"/static/js/vendor/sortable.min.js",
|
"/static/js/vendor/sortable.min.js",
|
||||||
"/static/icons/icon-192.png",
|
"/static/icons/icon-192.png",
|
||||||
"/static/icons/icon-512.png",
|
"/static/icons/icon-512.png",
|
||||||
|
"/static/icons/icon-apple-180.png",
|
||||||
"/static/manifest.json",
|
"/static/manifest.json",
|
||||||
|
"/static/offline.html",
|
||||||
];
|
];
|
||||||
|
|
||||||
self.addEventListener("install", (event) => {
|
self.addEventListener("install", (event) => {
|
||||||
@@ -50,7 +52,7 @@ self.addEventListener("fetch", (event) => {
|
|||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
})
|
})
|
||||||
.catch(() => caches.match(request))
|
.catch(() => caches.match(request).then((cached) => cached || caches.match("/static/offline.html")))
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@
|
|||||||
<title>{% block title %}습관 트래커{% endblock %}</title>
|
<title>{% block title %}습관 트래커{% endblock %}</title>
|
||||||
|
|
||||||
<link rel="manifest" href="/static/manifest.json" />
|
<link rel="manifest" href="/static/manifest.json" />
|
||||||
<meta name="theme-color" content="#d97757" />
|
<meta name="theme-color" content="#d97757" media="(prefers-color-scheme: light)" />
|
||||||
|
<meta name="theme-color" content="#e08962" media="(prefers-color-scheme: dark)" />
|
||||||
<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="default" />
|
||||||
<meta name="apple-mobile-web-app-title" content="습관 트래커" />
|
<meta name="apple-mobile-web-app-title" content="습관 트래커" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/static/icons/icon-apple-180.png" />
|
||||||
<link rel="apple-touch-icon" href="/static/icons/icon-192.png" />
|
<link rel="apple-touch-icon" href="/static/icons/icon-192.png" />
|
||||||
<link rel="icon" href="/static/icons/icon-192.png" />
|
<link rel="icon" href="/static/icons/icon-192.png" />
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if current_user %}
|
{% if current_user %}
|
||||||
<div class="nav-user">
|
<div class="nav-user">
|
||||||
|
{% if account_level %}<span class="nav-level-pill">Lv.{{ account_level.level }}</span>{% endif %}
|
||||||
<span>{{ current_user.name or current_user.email }}</span>
|
<span>{{ current_user.name or current_user.email }}</span>
|
||||||
<form method="post" action="/auth/logout">
|
<form method="post" action="/auth/logout">
|
||||||
<button type="submit" class="btn-link">로그아웃</button>
|
<button type="submit" class="btn-link">로그아웃</button>
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ def main() -> None:
|
|||||||
draw_icon(512).save(OUT_DIR / "icon-512.png")
|
draw_icon(512).save(OUT_DIR / "icon-512.png")
|
||||||
# 배경이 전체를 채우고 체크마크가 중앙 58%에만 있어 마스커블 세이프존(중앙 80%)을 만족한다.
|
# 배경이 전체를 채우고 체크마크가 중앙 58%에만 있어 마스커블 세이프존(중앙 80%)을 만족한다.
|
||||||
draw_icon(512).save(OUT_DIR / "icon-maskable-512.png")
|
draw_icon(512).save(OUT_DIR / "icon-maskable-512.png")
|
||||||
|
# iOS apple-touch-icon 권장 사이즈(180x180) — 192px를 그대로 쓰면 iOS가 다운스케일한다.
|
||||||
|
draw_icon(180).save(OUT_DIR / "icon-apple-180.png")
|
||||||
print(f"generated icons in {OUT_DIR}")
|
print(f"generated icons in {OUT_DIR}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user