{"version":3,"file":"InstallBanner.cjs","names":[],"sources":["../../../src/components/InstallBanner/InstallBanner.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — title, description, installLabel and dismissLabel\n * are four strings the app has to write, icon and className the look, storageKey the\n * dismissal memory and onResult the outcome. An install prompt with SDK-authored\n * copy is the one thing nobody ships.\n */\nimport { useId, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport { Button } from \"@/components/Button\";\nimport { useInstallPrompt } from \"@/hooks/use-install-prompt\";\nimport { defaultInstallHint } from \"@/components/InstallButton/install-hints\";\nimport type { RenderInstallHint } from \"@/components/InstallButton/install-hints\";\nimport type { InstallOutcome } from \"@/components/InstallButton\";\nimport styles from \"./InstallBanner.module.css\";\n\nexport interface InstallBannerProps {\n    /** Headline. Default `\"Instale o app\"`. */\n    title?: ReactNode;\n    /** Supporting copy under the title. */\n    description?: ReactNode;\n    /** Install button label. Default `\"Instalar\"`. */\n    installLabel?: string;\n    /** Accessible label for the dismiss button. Default `\"Dispensar\"`. */\n    dismissLabel?: string;\n    /** Optional leading icon. */\n    icon?: ReactNode;\n    /**\n     * `localStorage` key used to remember dismissal across reloads. Omit to\n     * make dismissal last only for the current session (component state).\n     */\n    storageKey?: string;\n    /**\n     * How long, in ms, a dismissal lasts before the banner may return.\n     *\n     * Omitted, a dismissal written to `storageKey` is permanent, which is what\n     * the component always did and what a stored `\"1\"` keeps meaning. With a\n     * cooldown the dismissal is stored as a timestamp instead, so the banner\n     * comes back after the window — the same shape `useInstallPrompt` uses for\n     * its own decline.\n     */\n    declineCooldownMs?: number;\n    /**\n     * Install button label used when the browser cannot be prompted and the\n     * banner shows the manual instruction. Default `\"Como instalar\"`.\n     */\n    hintLabel?: string;\n    /**\n     * Replaces the instruction shown for the `\"ios\"` and `\"manual\"` methods.\n     *\n     * The default copy is in PT-BR and names the real menu entries.\n     */\n    renderHint?: RenderInstallHint;\n    /**\n     * How long, in ms, to wait for `beforeinstallprompt` before falling back to\n     * the manual instruction. Forwarded to {@link useInstallPrompt}; defaults to\n     * 3000 there.\n     */\n    manualFallbackDelayMs?: number;\n    /** Called with the user's choice after the install prompt resolves. */\n    onResult?: (outcome: InstallOutcome) => void;\n    className?: string;\n}\n\n/**\n * Whether the user already dismissed this banner.\n *\n * Two stored shapes, and both have to keep working: `\"1\"` is the permanent\n * dismissal the component has always written, and a timestamp is what a banner\n * with `declineCooldownMs` writes. A `\"1\"` found while a cooldown is configured\n * still counts as dismissed forever — it was written under the old contract, and\n * reviving a banner the user turned off is worse than keeping it off.\n *\n * @param storageKey - The key holding the dismissal, if any.\n * @param declineCooldownMs - How long a timestamped dismissal lasts.\n * @returns Whether the banner should stay hidden.\n */\nfunction readDismissed(storageKey?: string, declineCooldownMs?: number): boolean {\n    if (!storageKey || typeof window === \"undefined\") return false;\n    try {\n        const raw = window.localStorage.getItem(storageKey);\n        if (!raw) return false;\n        if (raw === \"1\") return true;\n        const dismissedAt = Number(raw);\n        if (!Number.isFinite(dismissedAt)) return false;\n        if (declineCooldownMs === undefined) return true;\n        return Date.now() - dismissedAt < declineCooldownMs;\n    } catch {\n        return false;\n    }\n}\n\n/**\n * Dismissible bottom banner that invites the user to install the PWA, wired to\n * {@link useInstallPrompt}.\n *\n * Where the browser fires `beforeinstallprompt`, the button installs. Where it\n * does not — iOS Safari, and the Android Chromium forks that strip the API — the\n * banner shows that platform's instruction instead of disappearing. It used to\n * disappear: built on `useBeforeInstallPrompt`, it rendered `null` exactly where\n * the user has no other way to find the install entry, which is why every app\n * kept a local copy of this component just to add the two missing paths.\n *\n * @tempest-limits empty-catch — persisting the dismissal is a courtesy, not the\n * feature. When `localStorage` refuses the write (quota, private mode) the banner\n * still hides for this session; the worst case is that it comes back next visit,\n * which beats an error thrown out of a click handler that only closed a banner.\n *\n * @example\n * <InstallBanner\n *     title=\"Instale o FAMACHApp\"\n *     description=\"Acesso offline e atalho na tela inicial.\"\n *     storageKey=\"famacha:install-dismissed\"\n * />\n */\nexport function InstallBanner({\n    title = \"Instale o app\",\n    description,\n    installLabel = \"Instalar\",\n    hintLabel = \"Como instalar\",\n    dismissLabel = \"Dispensar\",\n    icon,\n    storageKey,\n    declineCooldownMs,\n    renderHint = defaultInstallHint,\n    manualFallbackDelayMs,\n    onResult,\n    className,\n}: InstallBannerProps) {\n    const { method, openInChromeIntent, install } = useInstallPrompt({ manualFallbackDelayMs });\n    const [dismissed, setDismissed] = useState<boolean>(() =>\n        readDismissed(storageKey, declineCooldownMs),\n    );\n    const [hintOpen, setHintOpen] = useState(false);\n    const hintId = useId();\n\n    if (method === \"none\" || dismissed) return null;\n\n    const dismiss = (): void => {\n        setDismissed(true);\n        if (storageKey && typeof window !== \"undefined\") {\n            try {\n                window.localStorage.setItem(\n                    storageKey,\n                    declineCooldownMs === undefined ? \"1\" : String(Date.now()),\n                );\n            } catch {\n                /* empty */\n            }\n        }\n    };\n\n    const hint = method === \"native\" ? null : renderHint({ method, openInChromeIntent });\n\n    return (\n        <div className={cn(styles.banner, className)} role=\"region\" aria-label={String(title)}>\n            {icon && <span className={styles.icon}>{icon}</span>}\n            <div className={styles.body}>\n                <p className={styles.title}>{title}</p>\n                {description && <p className={styles.description}>{description}</p>}\n                {hintOpen && hint ? (\n                    <p className={styles.hint} id={hintId}>\n                        {hint}\n                    </p>\n                ) : null}\n            </div>\n            {method === \"native\" ? (\n                <Button\n                    size=\"sm\"\n                    onClick={async () => {\n                        const accepted = await install();\n                        onResult?.(accepted ? \"accepted\" : \"dismissed\");\n                    }}\n                >\n                    {installLabel}\n                </Button>\n            ) : (\n                <Button\n                    size=\"sm\"\n                    aria-expanded={hintOpen}\n                    aria-controls={hintOpen && hint ? hintId : undefined}\n                    onClick={() => setHintOpen((open) => !open)}\n                >\n                    {hintLabel}\n                </Button>\n            )}\n            <button\n                type=\"button\"\n                className={styles.close}\n                aria-label={dismissLabel}\n                onClick={dismiss}\n            >\n                <X size={18} aria-hidden />\n            </button>\n        </div>\n    );\n}\n"],"mappings":"gSA8EA,SAAS,EAAc,EAAqB,EAAqC,CAC7E,GAAI,CAAC,GAAc,OAAO,OAAW,IAAa,MAAO,GACzD,GAAI,CACA,IAAM,EAAM,OAAO,aAAa,QAAQ,CAAU,EAClD,GAAI,CAAC,EAAK,MAAO,GACjB,GAAI,IAAQ,IAAK,MAAO,GACxB,IAAM,EAAc,OAAO,CAAG,EAG9B,OAFK,OAAO,SAAS,CAAW,EAC5B,IAAsB,IAAA,IACnB,KAAK,IAAI,EAAI,EAAc,EAFQ,EAG9C,MAAQ,CACJ,MAAO,EACX,CACJ,CAyBA,SAAgB,EAAc,CAC1B,QAAQ,gBACR,cACA,eAAe,WACf,YAAY,gBACZ,eAAe,YACf,OACA,aACA,oBACA,aAAa,EAAA,mBACb,wBACA,WACA,aACmB,CACnB,GAAM,CAAE,SAAQ,qBAAoB,WAAY,EAAA,iBAAiB,CAAE,uBAAsB,CAAC,EACpF,CAAC,EAAW,IAAA,EAAgB,EAAA,SAAA,KAC9B,EAAc,EAAY,CAAiB,CAC/C,EACM,CAAC,EAAU,IAAA,EAAe,EAAA,SAAA,CAAS,EAAK,EACxC,GAAA,EAAS,EAAA,MAAA,CAAM,EAErB,GAAI,IAAW,QAAU,EAAW,OAAO,KAE3C,IAAM,MAAsB,CAExB,GADA,EAAa,EAAI,EACb,GAAc,OAAO,OAAW,IAChC,GAAI,CACA,OAAO,aAAa,QAChB,EACA,IAAsB,IAAA,GAAY,IAAM,OAAO,KAAK,IAAI,CAAC,CAC7D,CACJ,MAAQ,CAER,CAER,EAEM,EAAO,IAAW,SAAW,KAAO,EAAW,CAAE,SAAQ,oBAAmB,CAAC,EAEnF,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,OAAQ,CAAS,EAAG,KAAK,SAAS,aAAY,OAAO,CAAK,EAApF,SAAA,CACK,IAAQ,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,KAAO,SAAA,CAAW,CAAA,GACnD,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAW,EAAA,QAAO,MAAQ,SAAA,CAAS,CAAA,EACrC,IAAe,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAW,EAAA,QAAO,YAAc,SAAA,CAAe,CAAA,EACjE,GAAY,GACT,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAW,EAAA,QAAO,KAAM,GAAI,EAC1B,SAAA,CACF,CAAA,EACH,IACH,IACJ,IAAW,UACR,EAAA,EAAA,IAAA,CAAC,EAAA,OAAD,CACI,KAAK,KACL,QAAS,SAAY,CACjB,IAAM,EAAW,MAAM,EAAQ,EAC/B,IAAW,EAAW,WAAa,WAAW,CAClD,EAEC,SAAA,CACG,CAAA,GAER,EAAA,EAAA,IAAA,CAAC,EAAA,OAAD,CACI,KAAK,KACL,gBAAe,EACf,gBAAe,GAAY,EAAO,EAAS,IAAA,GAC3C,YAAe,EAAa,GAAS,CAAC,CAAI,EAEzC,SAAA,CACG,CAAA,GAEZ,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,MAClB,aAAY,EACZ,QAAS,EAET,UAAA,EAAA,EAAA,IAAA,CAAC,EAAA,EAAD,CAAG,KAAM,GAAI,cAAA,EAAa,CAAA,CACtB,CAAA,CACP,GAEb"}