|
1 | | -import { useSyncExternalStore } from "react"; |
| 1 | +import { useCallback, useState, useSyncExternalStore } from "react"; |
2 | 2 |
|
3 | 3 | const MD_BREAKPOINT = 768; |
4 | 4 |
|
@@ -33,36 +33,58 @@ interface AdaptiveSvgProps { |
33 | 33 | src: string; |
34 | 34 | alt?: string; |
35 | 35 | className?: string; |
| 36 | + /** Use <img> instead of <object> on mobile (may cause shadow artifacts). */ |
| 37 | + mobileImg?: boolean; |
36 | 38 | } |
37 | 39 |
|
38 | | -export function AdaptiveSvg({ src, alt, className }: AdaptiveSvgProps) { |
| 40 | +export function AdaptiveSvg({ |
| 41 | + src, |
| 42 | + alt, |
| 43 | + className, |
| 44 | + mobileImg = false, |
| 45 | +}: AdaptiveSvgProps) { |
39 | 46 | const isDesktop = useIsDesktop(); |
40 | 47 | const url = resolveUrl(src); |
| 48 | + const useObject = isDesktop || !mobileImg; |
| 49 | + const [aspectRatio, setAspectRatio] = useState<string>(); |
| 50 | + |
| 51 | + const handleLoad = useCallback( |
| 52 | + (e: React.SyntheticEvent<HTMLObjectElement>) => { |
| 53 | + try { |
| 54 | + const svg = e.currentTarget.contentDocument?.documentElement; |
| 55 | + if (!svg) return; |
| 56 | + const vb = svg.getAttribute("viewBox"); |
| 57 | + if (!vb) return; |
| 58 | + const parts = vb |
| 59 | + .trim() |
| 60 | + .split(/[\s,]+/) |
| 61 | + .map(Number); |
| 62 | + if (parts.length === 4 && parts[2] > 0 && parts[3] > 0) { |
| 63 | + setAspectRatio(`${parts[2]} / ${parts[3]}`); |
| 64 | + } |
| 65 | + } catch { |
| 66 | + // cross-origin — ignore |
| 67 | + } |
| 68 | + }, |
| 69 | + [], |
| 70 | + ); |
41 | 71 |
|
42 | 72 | return ( |
43 | 73 | <div |
44 | 74 | className={`dark:hue-rotate-180 dark:invert${className ? ` ${className}` : ""}`} |
45 | 75 | > |
46 | | - {isDesktop ? ( |
47 | | - <div className="relative w-full"> |
48 | | - {/* Hidden <img> provides correct intrinsic sizing from the SVG viewBox, |
49 | | - since <object> can't derive its own height from SVG content. */} |
50 | | - <img |
51 | | - src={url} |
52 | | - alt="" |
53 | | - aria-hidden="true" |
54 | | - className="invisible block h-auto w-full" |
55 | | - /> |
56 | | - <object |
57 | | - data={url} |
58 | | - type="image/svg+xml" |
59 | | - aria-label={alt || src} |
60 | | - role="img" |
61 | | - className="absolute inset-0 block h-full w-full" |
62 | | - > |
63 | | - {alt || src} |
64 | | - </object> |
65 | | - </div> |
| 76 | + {useObject ? ( |
| 77 | + <object |
| 78 | + data={url} |
| 79 | + type="image/svg+xml" |
| 80 | + aria-label={alt || src} |
| 81 | + role="img" |
| 82 | + className="block h-auto w-full" |
| 83 | + style={aspectRatio ? { aspectRatio } : undefined} |
| 84 | + onLoad={handleLoad} |
| 85 | + > |
| 86 | + {alt || src} |
| 87 | + </object> |
66 | 88 | ) : ( |
67 | 89 | <img src={url} alt={alt || ""} className="block h-full w-full" /> |
68 | 90 | )} |
|
0 commit comments