mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
91 lines
3.2 KiB
TypeScript
91 lines
3.2 KiB
TypeScript
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 `<html data-glass>`, 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<GlassCapability>(() => ({
|
|
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;
|
|
};
|