|
| 1 | +--- |
| 2 | +interface Props { |
| 3 | + src: string; // URL string instead of imported asset |
| 4 | + alt: string; |
| 5 | + width?: number | string; |
| 6 | + height?: number | string; |
| 7 | + className?: string; |
| 8 | + loading?: "lazy" | "eager"; |
| 9 | + decoding?: "async" | "sync" | "auto"; |
| 10 | + sizes?: string; |
| 11 | + quality?: number; |
| 12 | + format?: "avif" | "webp" | "jpeg" | "jpg" | "png" | "gif"; |
| 13 | + // Additional HTML img attributes |
| 14 | + crossorigin?: "anonymous" | "use-credentials"; |
| 15 | + referrerpolicy?: |
| 16 | + | "no-referrer" |
| 17 | + | "no-referrer-when-downgrade" |
| 18 | + | "origin" |
| 19 | + | "origin-when-cross-origin" |
| 20 | + | "same-origin" |
| 21 | + | "strict-origin" |
| 22 | + | "strict-origin-when-cross-origin" |
| 23 | + | "unsafe-url"; |
| 24 | + // Style props |
| 25 | + objectFit?: "contain" | "cover" | "fill" | "none" | "scale-down"; |
| 26 | + objectPosition?: string; |
| 27 | +} |
| 28 | +
|
| 29 | +const { |
| 30 | + src, |
| 31 | + alt, |
| 32 | + width, |
| 33 | + height, |
| 34 | + className = "", |
| 35 | + loading = "lazy", |
| 36 | + decoding = "async", |
| 37 | + sizes, |
| 38 | + quality, |
| 39 | + format, |
| 40 | + crossorigin, |
| 41 | + referrerpolicy, |
| 42 | + objectFit, |
| 43 | + objectPosition, |
| 44 | +} = Astro.props; |
| 45 | +
|
| 46 | +// Build style object |
| 47 | +const imageStyles: Record<string, string> = {}; |
| 48 | +if (width && typeof width === "number") imageStyles.width = `${width}px`; |
| 49 | +if (width && typeof width === "string") imageStyles.width = width; |
| 50 | +if (height && typeof height === "number") imageStyles.height = `${height}px`; |
| 51 | +if (height && typeof height === "string") imageStyles.height = height; |
| 52 | +if (objectFit) imageStyles.objectFit = objectFit; |
| 53 | +if (objectPosition) imageStyles.objectPosition = objectPosition; |
| 54 | +
|
| 55 | +// Function to check if URL is external |
| 56 | +function isExternalUrl(url: string): boolean { |
| 57 | + return url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//"); |
| 58 | +} |
| 59 | +
|
| 60 | +// Function to optimize image URL based on format and quality (for external images) |
| 61 | +function optimizeImageUrl(url: string, format?: string, quality?: number): string { |
| 62 | + // For external URLs, we can't optimize them directly |
| 63 | + // But we can add query parameters if the service supports it |
| 64 | + // This is a placeholder for future enhancement |
| 65 | + return url; |
| 66 | +} |
| 67 | +
|
| 68 | +// Process the source URL |
| 69 | +const isExternal = isExternalUrl(src); |
| 70 | +const processedSrc = isExternal ? optimizeImageUrl(src, format, quality) : src; |
| 71 | +
|
| 72 | +// Build srcset for responsive images (basic implementation) |
| 73 | +function buildSrcSet(baseSrc: string): string | undefined { |
| 74 | + if (!sizes || isExternal) return undefined; |
| 75 | + |
| 76 | + // For internal images, we could generate different sizes |
| 77 | + // This is a simplified version - in a real implementation you might |
| 78 | + // want to use Astro's image optimization service |
| 79 | + return undefined; |
| 80 | +} |
| 81 | +
|
| 82 | +const srcset = buildSrcSet(processedSrc); |
| 83 | +--- |
| 84 | + |
| 85 | +<img |
| 86 | + src={processedSrc} |
| 87 | + alt={alt} |
| 88 | + class={`image-url ${className}`} |
| 89 | + style={imageStyles} |
| 90 | + loading={loading} |
| 91 | + decoding={decoding} |
| 92 | + sizes={sizes} |
| 93 | + srcset={srcset} |
| 94 | + crossorigin={crossorigin} |
| 95 | + referrerpolicy={referrerpolicy} |
| 96 | + {...(width && typeof width === "number" && { width })} |
| 97 | + {...(height && typeof height === "number" && { height })} |
| 98 | +/> |
| 99 | + |
| 100 | +<style lang="scss"> |
| 101 | + @use "../../styles/mixins" as *; |
| 102 | + |
| 103 | + .image-url { |
| 104 | + max-width: 100%; |
| 105 | + height: auto; |
| 106 | + display: block; |
| 107 | + |
| 108 | + // Default styling similar to Astro's Image component |
| 109 | + &:not([width]):not([height]) { |
| 110 | + width: 100%; |
| 111 | + height: auto; |
| 112 | + } |
| 113 | + |
| 114 | + // Responsive behavior |
| 115 | + @include break-down(md) { |
| 116 | + max-width: 100%; |
| 117 | + height: auto; |
| 118 | + } |
| 119 | + |
| 120 | + // Loading states |
| 121 | + &[loading="lazy"] { |
| 122 | + // Add any lazy loading specific styles if needed |
| 123 | + } |
| 124 | + |
| 125 | + &[loading="eager"] { |
| 126 | + // Add any eager loading specific styles if needed |
| 127 | + } |
| 128 | + |
| 129 | + // Object fit styles when specified |
| 130 | + &[style*="object-fit"] { |
| 131 | + object-fit: inherit; // Will be overridden by inline styles |
| 132 | + } |
| 133 | + |
| 134 | + // Error handling - show placeholder if image fails to load |
| 135 | + &[alt]:after { |
| 136 | + content: attr(alt); |
| 137 | + position: absolute; |
| 138 | + top: 50%; |
| 139 | + left: 50%; |
| 140 | + transform: translate(-50%, -50%); |
| 141 | + padding: 1rem; |
| 142 | + background: var(--surface-nav-bg, #f5f5f5); |
| 143 | + border: 1px solid var(--border-color, #ddd); |
| 144 | + border-radius: 4px; |
| 145 | + font-size: 0.875rem; |
| 146 | + color: var(--text-body-secondary, #666); |
| 147 | + text-align: center; |
| 148 | + display: none; |
| 149 | + } |
| 150 | + |
| 151 | + &:not([src]), |
| 152 | + &[src=""] { |
| 153 | + &:after { |
| 154 | + display: block; |
| 155 | + } |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + // Dark theme support |
| 160 | + .theme-dark .image-url { |
| 161 | + &[alt]:after { |
| 162 | + background: var(--surface-nav-bg-dark, #2a2a2a); |
| 163 | + border-color: var(--border-color-dark, #444); |
| 164 | + color: var(--text-body-secondary-dark, #ccc); |
| 165 | + } |
| 166 | + } |
| 167 | +</style> |
| 168 | + |
0 commit comments