diff --git a/src/components/AdminShell.tsx b/src/components/AdminShell.tsx
index dc27427..4f8ad16 100644
--- a/src/components/AdminShell.tsx
+++ b/src/components/AdminShell.tsx
@@ -12,6 +12,7 @@ import {
import { useNavigate } from "react-router-dom";
import { useAuth } from "../lib/auth";
import { initials, cn } from "../lib/format";
+import { InstallAppPrompt } from "./InstallAppPrompt";
interface NavItem {
to: string;
@@ -131,6 +132,9 @@ export function AdminShell() {
Admin
+
+
+
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index ffd95a0..e763e41 100644
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -19,6 +19,7 @@ import {
import { useAuth } from "../lib/auth";
import { initials, cn } from "../lib/format";
import { DemoSwitcher } from "./DemoSwitcher";
+import { InstallAppPrompt } from "./InstallAppPrompt";
interface NavItem {
to: string;
@@ -164,6 +165,9 @@ export function AppShell() {
AgendaMax
+
+
+
diff --git a/src/components/InstallAppPrompt.tsx b/src/components/InstallAppPrompt.tsx
new file mode 100644
index 0000000..9133381
--- /dev/null
+++ b/src/components/InstallAppPrompt.tsx
@@ -0,0 +1,29 @@
+import { useState } from "react";
+import { Download, PlusSquare, Share } from "lucide-react";
+import { Modal } from "./Modal";
+import { useInstallPrompt } from "../lib/useInstallPrompt";
+
+export function InstallAppPrompt() {
+ const { state, install } = useInstallPrompt();
+ const [iosOpen, setIosOpen] = useState(false);
+
+ if (state === "unsupported" || state === "installed") return null;
+ if (state === "ios-instructions") {
+ return (
+ <>
+
+ setIosOpen(false)} title="Instalar AgendaMax" subtitle="Safari lo añade a tu pantalla de inicio.">
+
+
+ - Toca Compartir en la barra de Safari.
+ - Elige Añadir a pantalla de inicio y confirma.
+
+
+
+ >
+ );
+ }
+ return ;
+}
diff --git a/src/index.css b/src/index.css
index 094a1a9..9769647 100644
--- a/src/index.css
+++ b/src/index.css
@@ -281,6 +281,9 @@ body {
.btn-danger:hover:not(:disabled) {
background: #fecaca;
}
+.safe-area-bottom {
+ padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
+}
@layer components {
.input,
diff --git a/src/lib/useInstallPrompt.ts b/src/lib/useInstallPrompt.ts
new file mode 100644
index 0000000..57513a2
--- /dev/null
+++ b/src/lib/useInstallPrompt.ts
@@ -0,0 +1,65 @@
+import { useEffect, useState } from "react";
+
+export type InstallPromptState = "unsupported" | "available" | "ios-instructions" | "installed";
+
+interface BeforeInstallPromptEvent extends Event {
+ prompt: () => Promise;
+ userChoice: Promise<{ outcome: "accepted" | "dismissed"; platform: string }>;
+}
+
+function isStandalone() {
+ return window.matchMedia("(display-mode: standalone)").matches ||
+ ("standalone" in navigator && Boolean((navigator as Navigator & { standalone?: boolean }).standalone));
+}
+
+function isIOSSafari() {
+ const ua = navigator.userAgent;
+ const ios = /iPad|iPhone|iPod/.test(ua) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
+ return ios && /Safari/i.test(ua) && !/CriOS|FxiOS|EdgiOS|OPiOS/i.test(ua);
+}
+
+export function useInstallPrompt() {
+ const [state, setState] = useState(() => {
+ if (typeof window === "undefined") return "unsupported";
+ if (isStandalone()) return "installed";
+ return isIOSSafari() ? "ios-instructions" : "unsupported";
+ });
+ const [deferred, setDeferred] = useState(null);
+ const [dismissed, setDismissed] = useState(false);
+
+ useEffect(() => {
+ if (isStandalone()) {
+ setState("installed");
+ return;
+ }
+
+ const onBeforeInstallPrompt = (event: Event) => {
+ event.preventDefault();
+ setDeferred(event as BeforeInstallPromptEvent);
+ setState("available");
+ };
+ const onInstalled = () => {
+ setDeferred(null);
+ setState("installed");
+ };
+
+ window.addEventListener("beforeinstallprompt", onBeforeInstallPrompt);
+ window.addEventListener("appinstalled", onInstalled);
+ return () => {
+ window.removeEventListener("beforeinstallprompt", onBeforeInstallPrompt);
+ window.removeEventListener("appinstalled", onInstalled);
+ };
+ }, []);
+
+ const install = async () => {
+ if (!deferred) return;
+ const event = deferred;
+ setDeferred(null);
+ await event.prompt();
+ const choice = await event.userChoice;
+ if (choice.outcome === "accepted") setState("installed");
+ else setDismissed(true);
+ };
+
+ return { state: dismissed ? "unsupported" : state, install, dismiss: () => setDismissed(true) };
+}
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx
index 549a9d2..39f8405 100644
--- a/src/pages/LoginPage.tsx
+++ b/src/pages/LoginPage.tsx
@@ -4,6 +4,7 @@ import { Sparkles, ArrowRight, Mail, Lock, AlertCircle, Wand2 } from "lucide-rea
import { useAuth } from "../lib/auth";
import { api } from "../lib/api";
import { Spinner } from "../components/ui";
+import { InstallAppPrompt } from "../components/InstallAppPrompt";
const DEMO = import.meta.env.DEV || import.meta.env.VITE_DEMO_UI === "1";
@@ -156,6 +157,10 @@ export function LoginPage() {
+
+
+
+
{DEMO && (
<>