fix(ui): botones alineados al cuerpo, FAB grande en PC y botón superior de propuesta
- FAB y aviso PWA flanquean la columna central (720px): right/left max(16px, calc((100vw - 720px)/2 + 16px)) para acompañar al contenido en pantallas anchas sin pegarse al borde de la ventana - FAB de 64px en PC (54 en móvil); PWA abajo a la izquierda en PC y móvil - Nuevo BotonProponerArriba (solo PC): píldora "Proponer local" arriba a la derecha, alineada al cuerpo, que abre la misma pasarela
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import usePwaInstall from "../hooks/usePwaInstall.js";
|
||||
import useEsMovil from "../hooks/useEsMovil.js";
|
||||
import { ANCHO_CUERPO } from "../styles/shared.js";
|
||||
|
||||
const CLAVE_VISITAS = "localesp_visitas";
|
||||
const CLAVE_CERRADO = "localesp_banner_cerrado_hasta";
|
||||
@@ -33,12 +34,12 @@ function estaCerradoRecientemente() {
|
||||
// modo standalone (o el navegador confirma que está instalada), el aviso
|
||||
// se oculta porque ya no aporta nada mostrarlo.
|
||||
//
|
||||
// Colocación (no debe tapar el FAB de "proponer local", abajo a la derecha):
|
||||
// - Móvil: tarjeta completa abajo a la IZQUIERDA, respetando la barra
|
||||
// inferior de la PWA (env(safe-area-inset-bottom)).
|
||||
// - PC: botón compacto arriba a la izquierda; al pulsarlo instala
|
||||
// directamente o, si el navegador no soporta el diálogo nativo,
|
||||
// despliega la tarjeta con las instrucciones.
|
||||
// Colocación: abajo a la IZQUIERDA en PC y móvil, alineada con el borde
|
||||
// izquierdo del cuerpo central (columna de ANCHO_CUERPO px), flanqueando el
|
||||
// contenido junto al FAB de "proponer local" (abajo a la derecha): en
|
||||
// pantallas anchas acompaña a la columna en vez de pegarse al borde de la
|
||||
// ventana; en estrechas queda a 16px. Respeta la barra inferior de la PWA
|
||||
// (env(safe-area-inset-bottom)) y en PC se muestra algo más grande.
|
||||
//
|
||||
// Para no ser intrusivo, solo se muestra a partir de la 2ª visita del
|
||||
// usuario (contador en localStorage) y, si se cierra con ✕, no vuelve a
|
||||
@@ -46,7 +47,6 @@ function estaCerradoRecientemente() {
|
||||
export default function BannerMovil() {
|
||||
const [cerrado, setCerrado] = useState(estaCerradoRecientemente);
|
||||
const [visitasSuficientes, setVisitasSuficientes] = useState(false);
|
||||
const [desplegada, setDesplegada] = useState(false); // tarjeta expandida bajo el botón en PC
|
||||
const { isInstalled, canPromptInstall, promptInstall, isIos } = usePwaInstall();
|
||||
const [instalando, setInstalando] = useState(false);
|
||||
const [resultado, setResultado] = useState(null); // "accepted" | "dismissed" | null
|
||||
@@ -70,11 +70,8 @@ export default function BannerMovil() {
|
||||
const outcome = await promptInstall();
|
||||
setResultado(outcome);
|
||||
setInstalando(false);
|
||||
if (outcome === "dismissed") setDesplegada(true); // se muestra el mensaje "más tarde"
|
||||
};
|
||||
|
||||
// Tarjeta completa (instrucciones / instalación). `onCerrarTarjeta`
|
||||
// difiere: en móvil aplica la regla de 14 días; en PC solo la recoge.
|
||||
const tarjeta = (onCerrarTarjeta, estiloExtra = {}) => (
|
||||
<div style={{ position: "relative", background: "white", border: "1px solid #E5DDD5", borderRadius: 14, padding: "14px 18px", boxShadow: "0 6px 24px rgba(0,0,0,0.10)", width: 280, boxSizing: "border-box", fontFamily: "var(--font-sans)", ...estiloExtra }}>
|
||||
<button onClick={onCerrarTarjeta} aria-label="Cerrar aviso de instalación"
|
||||
@@ -113,30 +110,16 @@ export default function BannerMovil() {
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── Móvil: tarjeta abajo a la izquierda (el FAB está abajo a la derecha) ──
|
||||
if (esMovil) {
|
||||
return (
|
||||
<div style={{ position: "fixed", left: 16, bottom: 0, zIndex: 999, paddingBottom: "env(safe-area-inset-bottom)" }}>
|
||||
{tarjeta(handleCerrar, { marginTop: 16 })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── PC: botón compacto arriba a la izquierda ──
|
||||
// ── Aviso abajo a la izquierda (PC y móvil), alineado con el borde del cuerpo ──
|
||||
return (
|
||||
<div style={{ position: "fixed", top: 20, left: 20, zIndex: 999 }}>
|
||||
<div style={{ display: "flex", alignItems: "stretch", gap: 6 }}>
|
||||
<button
|
||||
onClick={canPromptInstall ? handleInstalar : () => setDesplegada((v) => !v)}
|
||||
disabled={instalando}
|
||||
aria-expanded={desplegada}
|
||||
style={{ display: "flex", alignItems: "center", gap: 7, background: "#8B0000", color: "white", border: "none", borderRadius: 999, padding: "9px 16px", fontSize: 13, fontWeight: 700, cursor: instalando ? "default" : "pointer", fontFamily: "var(--font-sans)", boxShadow: "0 4px 14px rgba(139,0,0,0.30)", opacity: instalando ? 0.7 : 1 }}>
|
||||
{instalando ? "Abriendo instalación..." : "📲 Instalar app"}
|
||||
</button>
|
||||
<button onClick={handleCerrar} aria-label="Ocultar aviso de instalación (no volverá a mostrarse en 14 días)" title="No mostrar más durante 14 días"
|
||||
style={{ background: "white", border: "1px solid #E5DDD5", borderRadius: 999, width: 30, cursor: "pointer", color: "#999", fontSize: 13, boxShadow: "0 4px 14px rgba(0,0,0,0.08)" }}>✕</button>
|
||||
</div>
|
||||
{desplegada && tarjeta(() => setDesplegada(false), { marginTop: 8 })}
|
||||
<div style={{
|
||||
position: "fixed",
|
||||
left: `max(16px, calc((100vw - ${ANCHO_CUERPO}px) / 2 + 16px))`,
|
||||
bottom: 0,
|
||||
zIndex: 999,
|
||||
paddingBottom: "calc(16px + env(safe-area-inset-bottom))",
|
||||
}}>
|
||||
{tarjeta(handleCerrar, { width: esMovil ? 280 : 320 })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import { useState } from "react";
|
||||
import useEsMovil from "../hooks/useEsMovil.js";
|
||||
import { ANCHO_CUERPO } from "../styles/shared.js";
|
||||
|
||||
const TEXTO_TOOLTIP = "Añade un nuevo local al directorio";
|
||||
|
||||
// Botón de acción flotante (FAB) para proponer un local: fijo en la esquina
|
||||
// inferior derecha, visible en PC y móvil, respetando la barra de la PWA con
|
||||
// env(safe-area-inset-bottom). Su z-index queda por debajo del modal de la
|
||||
// pasarela (1000) y del toast (2000). Muestra un tooltip propio al hacer
|
||||
// hover o recibir foco (posicionado a la izquierda) y expone aria-label
|
||||
// para accesibilidad sin ratón.
|
||||
// Botón de acción flotante (FAB) para proponer un local: fijo abajo a la
|
||||
// derecha, visible en PC y móvil, alineado con el borde derecho del cuerpo
|
||||
// central (columna de ANCHO_CUERPO px) para que acompañe al contenido en
|
||||
// pantallas anchas. En PC es más grande (64px) que en móvil (54px). Respeta
|
||||
// la barra de la PWA con env(safe-area-inset-bottom). Muestra un tooltip
|
||||
// propio al hacer hover o recibir foco (posicionado a la izquierda) y expone
|
||||
// aria-label para accesibilidad sin ratón.
|
||||
export default function BotonFlotante({ onClick }) {
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||
const esMovil = useEsMovil();
|
||||
const tamano = esMovil ? 54 : 64; // más grande en PC
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
right: 16,
|
||||
// Abajo a la derecha, alineado con el borde derecho del cuerpo central:
|
||||
// en pantallas anchas flanquea la columna de contenido; en estrechas, 16px.
|
||||
right: `max(16px, calc((100vw - ${ANCHO_CUERPO}px) / 2 + 16px))`,
|
||||
bottom: 0,
|
||||
zIndex: 900,
|
||||
paddingBottom: "env(safe-area-inset-bottom)",
|
||||
@@ -32,10 +39,10 @@ export default function BotonFlotante({ onClick }) {
|
||||
onBlur={() => setTooltipVisible(false)}
|
||||
style={{
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
width: 54, height: 54, borderRadius: "50%",
|
||||
width: tamano, height: tamano, borderRadius: "50%",
|
||||
background: "#8B0000", color: "white", border: "none",
|
||||
boxShadow: "0 4px 14px rgba(139,0,0,0.4)",
|
||||
fontSize: 24, cursor: "pointer",
|
||||
boxShadow: esMovil ? "0 4px 14px rgba(139,0,0,0.4)" : "0 6px 20px rgba(139,0,0,0.45)",
|
||||
fontSize: esMovil ? 24 : 28, cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<i className="ti ti-plus" aria-hidden="true"></i>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import useEsMovil from "../hooks/useEsMovil.js";
|
||||
import { ANCHO_CUERPO } from "../styles/shared.js";
|
||||
|
||||
// Botón "Proponer local" arriba a la derecha (solo PC): hace exactamente lo
|
||||
// mismo que el FAB inferior — abre la pasarela de propuesta — pero da un
|
||||
// punto de acceso visible en la zona superior, alineado con el borde derecho
|
||||
// del cuerpo central (columna de ANCHO_CUERPO px) para acompañar al contenido
|
||||
// en pantallas anchas. En móvil no se renderiza (ahí basta el FAB inferior).
|
||||
export default function BotonProponerArriba({ onClick }) {
|
||||
const esMovil = useEsMovil();
|
||||
if (esMovil) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-label="Añade un nuevo local al directorio"
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 20,
|
||||
right: `max(16px, calc((100vw - ${ANCHO_CUERPO}px) / 2 + 16px))`,
|
||||
zIndex: 900,
|
||||
display: "flex", alignItems: "center", gap: 8,
|
||||
background: "#8B0000", color: "white", border: "none", borderRadius: 999,
|
||||
padding: "11px 22px", fontSize: 15, fontWeight: 600,
|
||||
cursor: "pointer", fontFamily: "var(--font-sans)",
|
||||
boxShadow: "0 4px 14px rgba(139,0,0,0.35)",
|
||||
}}
|
||||
>
|
||||
<i className="ti ti-plus" aria-hidden="true"></i> Proponer local
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user