Dà profondità alle superfici e trasforma la barra in vetro
Fondo pagina, carte e `secondary` stavano dentro tre punti di luminosità:
carta e fondo avevano un rapporto di contrasto di 1.04, cioè erano lo stesso
bianco. Le carte non si leggevano come carte, e l'unica cosa che le staccava
era `shadow-card` — 6% di nero, che su un telefono al sole non esiste. Da qui
l'impressione che fondo, barra ed elementi fossero «tre bianchi».
La rampa dei neutri è ridistanziata: fondo 0.93, carta 1.0, `secondary` 0.90.
Carta e fondo passano a 1.23, un elemento dentro una carta sta a 1.35 dalla
carta ed è più scuro di essa, come nelle liste raggruppate di iOS. 0.93 è il
fondo più scuro possibile senza toccare altro: il limite lo impone
`text-accent` appoggiato sul fondo, che a 0.92 scenderebbe sotto 4.5.
Due colori vanno corretti di conseguenza, altrimenti la modifica peggiora la
leggibilità: l'accento scende a 0.54 (a 0.58 come testo faceva 4.07 sul nuovo
fondo; ora fa 4.62, e migliora anche il bianco che ci sta sopra, da 4.65 a
5.51) e `muted-foreground` a 0.48. Verificati convertendo le oklch in sRGB e
calcolando i rapporti, non a occhio.
`--destructive` e `--ring` erano *lo stesso identico colore* dell'accento:
«elimina», «selezionato» e «ho il focus qui» davano il medesimo segnale. Il
distruttivo resta rosso ma più scuro e cupo, il focus viene dal neutro.
Le ombre passano da una a tre quote: `shadow-card` per il contenuto,
`shadow-chrome` per barra, drawer e toast (più profonda, più il filo chiaro sul
bordo alto), `shadow-alza` neutra per l'hover — `premi` metteva un alone rosso
sotto ogni riga di classifica.
La barra ora usa `vetro`: fondo al 62% invece dell'85%, così ci si vede
attraverso, più tre ombre interne che sono il bordo illuminato. L'85% di
`materiale` esiste per tenere allineati WebKit e Blink (608f2ed) su una fascia
larga quanto lo schermo; su una pillola di 60 px lo scarto non si legge.
Dietro `@supports`, un `feDisplacementMap` distorce davvero il fondale: solo
Blink accetta un filtro SVG in `backdrop-filter`, e WebKit ignorando l'url
scarterebbe anche il blur. Miglioria progressiva, non l'effetto principale.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { CalendarDays, Home, Trophy, Users } from "lucide-react";
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { molla } from "@/lib/molla";
|
||||
|
||||
// Quattro voci e non cinque: il profilo sta in alto a destra
|
||||
// nell'intestazione di ogni pagina, dove lo cerca chi arriva da iOS.
|
||||
@@ -11,6 +13,8 @@ const items = [
|
||||
] as const;
|
||||
|
||||
export function BottomNav() {
|
||||
const ridotto = useReducedMotion();
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label="Navigazione principale"
|
||||
@@ -20,31 +24,75 @@ export function BottomNav() {
|
||||
// arrivare al contenuto sotto.
|
||||
className="pad-sicura-fondo pointer-events-none fixed inset-x-0 bottom-0 z-40 px-3"
|
||||
>
|
||||
{/*
|
||||
La mappa di spostamento del vetro. `feTurbulence` a bassa frequenza dà
|
||||
una deformazione lenta e irregolare — la stessa cosa che fa un vetro non
|
||||
perfettamente piano — e la sfocatura che segue toglie il granuloso del
|
||||
rumore, altrimenti il fondale si sgranerebbe invece di piegarsi.
|
||||
|
||||
Nessuna dimensione e nessun colore: è solo la definizione del filtro che
|
||||
il CSS richiama per id. La usa solo Blink (vedi `@supports` in
|
||||
styles.css); su WebKit questo SVG resta lì senza fare niente.
|
||||
*/}
|
||||
<svg aria-hidden="true" className="absolute h-0 w-0" focusable="false">
|
||||
<filter id="vetro-rifrazione" colorInterpolationFilters="sRGB">
|
||||
<feTurbulence
|
||||
type="fractalNoise"
|
||||
baseFrequency="0.008 0.016"
|
||||
numOctaves={2}
|
||||
seed={7}
|
||||
result="rumore"
|
||||
/>
|
||||
<feGaussianBlur in="rumore" stdDeviation={3} result="mappa" />
|
||||
<feDisplacementMap
|
||||
in="SourceGraphic"
|
||||
in2="mappa"
|
||||
scale={16}
|
||||
xChannelSelector="R"
|
||||
yChannelSelector="G"
|
||||
/>
|
||||
</filter>
|
||||
</svg>
|
||||
|
||||
{/*
|
||||
Altezza fissa dal token, non dedotta dal contenuto: la striscia
|
||||
toccabile misura uguale su iOS e Android, e sotto varia solo l'inset
|
||||
di sistema. Prima l'altezza dipendeva dai padding delle voci e nessuno
|
||||
poteva saperla da fuori.
|
||||
*/}
|
||||
<div className="materiale pointer-events-auto mx-auto grid h-[var(--altezza-nav)] max-w-md grid-cols-4 overflow-hidden rounded-full border border-border/60 px-1 shadow-lg">
|
||||
{/* `vetro` porta con sé le proprie ombre, quindi niente `shadow-chrome`:
|
||||
sarebbero due `box-shadow` sullo stesso elemento e una vincerebbe. */}
|
||||
<div className="vetro pointer-events-auto mx-auto grid h-[var(--altezza-nav)] max-w-md grid-cols-4 rounded-full border border-white/40 p-1.5">
|
||||
{items.map(({ to, label, icon: Icon }) => (
|
||||
<Link
|
||||
key={to}
|
||||
to={to}
|
||||
activeOptions={{ exact: to === "/" }}
|
||||
activeProps={{ "aria-current": "page" }}
|
||||
// Lo stato attivo non è solo colore: cambia anche il peso del
|
||||
// testo e compare la barretta sopra l'icona.
|
||||
className="group relative flex h-full flex-col items-center justify-center gap-1 text-xs font-semibold text-muted-foreground transition-colors data-[status=active]:font-extrabold data-[status=active]:text-accent"
|
||||
// Lo stato attivo non è solo colore: è la capsula piena sotto la
|
||||
// voce, più il peso del testo. Chi non distingue i colori vede
|
||||
// comunque quale voce è quella corrente.
|
||||
className="group relative flex h-full flex-col items-center justify-center gap-0.5 rounded-full text-xs font-semibold text-muted-foreground transition-colors data-[status=active]:font-extrabold data-[status=active]:text-accent-foreground"
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-x-4 top-0 h-0.5 rounded-full bg-accent opacity-0 transition-opacity group-data-[status=active]:opacity-100"
|
||||
/>
|
||||
<Icon className="h-5 w-5" strokeWidth={isActive ? 2.6 : 2.2} />
|
||||
{label}
|
||||
{isActive ? (
|
||||
<motion.span
|
||||
// `layoutId`: la capsula è un solo elemento che scivola da
|
||||
// una voce all'altra invece di sparire e ricomparire. La
|
||||
// molla è interrompibile, quindi due tocchi rapidi non
|
||||
// fanno saltare la posizione.
|
||||
layoutId="capsula-nav"
|
||||
aria-hidden="true"
|
||||
className="bg-accent-grad absolute inset-0 rounded-full shadow-pop"
|
||||
transition={ridotto ? { duration: 0 } : molla.ui}
|
||||
/>
|
||||
) : null}
|
||||
{/* Sopra la capsula, che è in flusso normale sotto di loro. */}
|
||||
<span className="relative flex flex-col items-center gap-0.5">
|
||||
<Icon className="h-5 w-5" strokeWidth={isActive ? 2.6 : 2.2} />
|
||||
{label}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
Reference in New Issue
Block a user