Redesign UI with zen cards, split AI panel, and PWA install support.

Learn uses 64-gua card grid; liuyao/bazi/combined use two input cards plus sticky right AI panel; add manifest, service worker, and install prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-10 23:24:55 +08:00
parent 206673fd90
commit 6265e56a7f
20 changed files with 682 additions and 423 deletions
+96
View File
@@ -0,0 +1,96 @@
"use client";
import { useEffect, useState } from "react";
import { Download, X } from "lucide-react";
import { Button } from "@/components/ui/button";
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
}
export default function PwaProvider() {
const [deferred, setDeferred] = useState<BeforeInstallPromptEvent | null>(
null,
);
const [dismissed, setDismissed] = useState(false);
const [isIos, setIsIos] = useState(false);
const [isStandalone, setIsStandalone] = useState(false);
useEffect(() => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js").catch(() => {});
}
setIsStandalone(
window.matchMedia("(display-mode: standalone)").matches ||
(window.navigator as Navigator & { standalone?: boolean }).standalone ===
true,
);
const ua = window.navigator.userAgent;
setIsIos(/iPad|iPhone|iPod/.test(ua));
const handler = (e: Event) => {
e.preventDefault();
setDeferred(e as BeforeInstallPromptEvent);
};
window.addEventListener("beforeinstallprompt", handler);
return () => window.removeEventListener("beforeinstallprompt", handler);
}, []);
if (isStandalone || dismissed) {
return null;
}
if (deferred) {
return (
<div className="fixed bottom-4 left-4 right-4 z-50 mx-auto flex max-w-md items-center gap-3 rounded-xl border border-border/60 bg-card/95 p-4 shadow-lg backdrop-blur-md sm:left-auto">
<Download size={20} className="shrink-0 text-primary" />
<div className="min-w-0 flex-1 text-sm">
<p className="font-medium"></p>
<p className="text-xs text-muted-foreground">
App 使
</p>
</div>
<Button
size="sm"
onClick={async () => {
await deferred.prompt();
setDeferred(null);
}}
>
</Button>
<button
type="button"
aria-label="关闭"
className="text-muted-foreground hover:text-foreground"
onClick={() => setDismissed(true)}
>
<X size={16} />
</button>
</div>
);
}
if (isIos) {
return (
<div className="fixed bottom-4 left-4 right-4 z-50 mx-auto max-w-md rounded-xl border border-border/60 bg-card/95 p-4 text-sm shadow-lg backdrop-blur-md sm:left-auto">
<p className="font-medium"> iPhone / iPad</p>
<p className="mt-1 text-xs text-muted-foreground">
Safari
</p>
<button
type="button"
className="mt-2 text-xs text-muted-foreground underline"
onClick={() => setDismissed(true)}
>
</button>
</div>
);
}
return null;
}