SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
3.8 KB · 84 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { Download, Share, SquarePlus, X } from "lucide-react";4import { Button } from "@/components/ui/button";5import { useLocalStorage, useMounted, useStandalone } from "@/lib/client/hooks";6import { cn } from "@/lib/utils";78interface BeforeInstallPromptEvent extends Event {9  prompt: () => Promise<void>;10  userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;11}1213function isIOS() {14  if (typeof navigator === "undefined") return false;15  const ua = navigator.userAgent;16  return /iPhone|iPad|iPod/.test(ua) || (ua.includes("Mac") && "ontouchend" in document);17}1819/**20 * "Install PolyLLM" hint. Uses `beforeinstallprompt` where the browser supports it (Chromium,21 * Android) and shows the Share → Add to Home Screen tip on iOS Safari. Hidden when already22 * installed (standalone) or after the visitor dismisses it. Marketing only — Settings → Appearance23 * has its own hint.24 */25export function InstallHint({ className, compact }: { className?: string; compact?: boolean }) {26  const mounted = useMounted();27  const standalone = useStandalone();28  const [dismissed, setDismissed] = useLocalStorage<boolean>("polyllm:install-hint-dismissed", false);29  const [deferred, setDeferred] = React.useState<BeforeInstallPromptEvent | null>(null);30  const [installed, setInstalled] = React.useState(false);31  const [ios, setIos] = React.useState(false);3233  React.useEffect(() => {34    // eslint-disable-next-line react-hooks/set-state-in-effect35    setIos(isIOS());36    const onPrompt = (e: Event) => {37      e.preventDefault();38      setDeferred(e as BeforeInstallPromptEvent);39    };40    const onInstalled = () => setInstalled(true);41    window.addEventListener("beforeinstallprompt", onPrompt);42    window.addEventListener("appinstalled", onInstalled);43    return () => {44      window.removeEventListener("beforeinstallprompt", onPrompt);45      window.removeEventListener("appinstalled", onInstalled);46    };47  }, []);4849  if (!mounted || standalone || dismissed || installed) return null;50  if (!deferred && !ios) return null;5152  const install = async () => {53    if (!deferred) return;54    await deferred.prompt();55    const { outcome } = await deferred.userChoice;56    if (outcome === "accepted") setInstalled(true);57    setDeferred(null);58  };5960  return (61    <div role="note" className={cn("relative flex items-start gap-3 rounded-2xl bg-bg-elevated p-4 shadow-xs ring-1 ring-border", compact && "p-3", className)}>62      <span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-accent-soft text-accent">{deferred ? <Download className="size-4.5" /> : <SquarePlus className="size-4.5" />}</span>63      <div className="min-w-0 flex-1">64        <p className="text-[14px] font-semibold tracking-tight">Install PolyLLM</p>65        {deferred ? (66          <p className="mt-0.5 text-[13px] leading-5 text-fg-muted">Add it to your home screen or dock: it opens full-screen, remembers your session and feels like a native app.</p>67        ) : (68          <p className="mt-0.5 text-[13px] leading-5 text-fg-muted">69            In Safari, tap <Share className="mx-0.5 inline size-3.5 -translate-y-px" aria-label="Share" /> <span className="font-medium text-fg">Share</span>, then <span className="font-medium text-fg">Add to Home Screen</span>. PolyLLM opens full-screen with its own icon.70          </p>71        )}72        {deferred ? (73          <Button size="sm" className="mt-2.5 h-9 sm:h-8" onClick={install}>74            <Download /> Install75          </Button>76        ) : null}77      </div>78      <button type="button" onClick={() => setDismissed(true)} className="tap -mr-1 -mt-1 rounded-md p-1.5 text-fg-subtle transition-colors hover:bg-bg-muted hover:text-fg" aria-label="Dismiss install hint">79        <X className="size-4" />80      </button>81    </div>82  );83}84