import { useEffect, useState } from 'react'; export type GlassTier = 'refract' | 'blur' | 'flat'; export interface GlassCapability { /** Which visual tier the document is currently rendering at. */ tier: GlassTier; /** True when SVG rim refraction is live. */ refract: boolean; /** True when the user asked for reduced motion — aurora + canvas must halt. */ reducedMotion: boolean; } const mq = (query: string): MediaQueryList | null => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return null; try { return window.matchMedia(query); } catch { return null; } }; /** * Can this engine actually render `backdrop-filter: url(#svg-filter)`? * * `CSS.supports` alone is not enough: `url()` is syntactically valid in a * filter-function-list, so Firefox and Safari both parse it happily and then * paint nothing — a silent failure that would leave their rims invisible * rather than gracefully flat. Chromium is currently the only engine that * renders it, so we gate on a Chromium signal too. `userAgentData` is a real * feature-detectable API rather than UA-string sniffing, and when it is absent * we simply fall back a tier — which is a designed look, not a broken one. */ const supportsRefraction = (): boolean => { if (typeof window === 'undefined' || typeof CSS === 'undefined' || !CSS.supports) return false; const parses = CSS.supports('backdrop-filter', 'url(#x)') || CSS.supports('-webkit-backdrop-filter', 'url(#x)'); if (!parses) return false; // mask-composite carries the rim ring; without it there is no ring to refract. const canMask = CSS.supports('mask-composite', 'exclude') || CSS.supports('-webkit-mask-composite', 'xor'); if (!canMask) return false; const brands = (navigator as Navigator & { userAgentData?: { brands?: { brand: string }[] } }) .userAgentData?.brands; return Array.isArray(brands) && brands.some((b) => /Chromium/i.test(b.brand)); }; const resolveTier = (): GlassTier => { if (mq('(prefers-reduced-transparency: reduce)')?.matches) return 'flat'; return supportsRefraction() ? 'refract' : 'blur'; }; /** * Resolves the glass tier and stamps it on ``, which is what * every refraction rule in `Glass.tsx` keys off. Call once, high in the tree. */ export const useGlassCapability = (): GlassCapability => { const [state, setState] = useState(() => ({ tier: 'blur', refract: false, reducedMotion: false, })); useEffect(() => { const transparency = mq('(prefers-reduced-transparency: reduce)'); const motion = mq('(prefers-reduced-motion: reduce)'); const apply = () => { const tier = resolveTier(); const reducedMotion = !!motion?.matches; document.documentElement.dataset.glass = tier; setState((prev) => prev.tier === tier && prev.reducedMotion === reducedMotion ? prev : { tier, refract: tier === 'refract', reducedMotion }, ); }; apply(); transparency?.addEventListener?.('change', apply); motion?.addEventListener?.('change', apply); return () => { transparency?.removeEventListener?.('change', apply); motion?.removeEventListener?.('change', apply); }; }, []); return state; };