a tool for shared writing and social publishing
0

Configure Feed

Select the types of activity you want to include in your feed.

Add alt text to image blocks! Update/image (#148)

* added alt text editor

* added styling to the read only and post view

* tried to add context to image alt editor to get it to open when alt toolbar button is clicked, but failed, gave up.

* i renamed shit to make it work but it didnt work

* put alt text popover state in zustand

* add alt attribute to rendered post images

---------

Co-authored-by: celine <celine@hyperlink.academy>

authored by

Jared Pereira
celine
and committed by
GitHub
(Jun 30, 2025, 8:58 PM EDT) 15f508eb a554367e

+197 -14
+2
actions/publishToPublication.ts
··· 249 249 if (b.type == "image") { 250 250 let [image] = scan.eav(b.value, "block/image"); 251 251 if (!image) return; 252 + let [altText] = scan.eav(b.value, "image/alt"); 252 253 let blobref = imageMap.get(image.data.src); 253 254 if (!blobref) return; 254 255 let block: $Typed<PubLeafletBlocksImage.Main> = { ··· 258 259 height: image.data.height, 259 260 width: image.data.width, 260 261 }, 262 + alt: altText ? altText.data.value : undefined, 261 263 }; 262 264 return block; 263 265 }
+2
components/Popover.tsx
··· 10 10 disabled?: boolean; 11 11 children: React.ReactNode; 12 12 align?: "start" | "end" | "center"; 13 + side?: "top" | "bottom" | "left" | "right"; 13 14 background?: string; 14 15 border?: string; 15 16 className?: string; ··· 43 44 overflow-y-scroll no-scrollbar 44 45 ${props.className} 45 46 `} 47 + side={props.side} 46 48 align={props.align ? props.align : "center"} 47 49 sideOffset={4} 48 50 collisionPadding={16}
+4
src/useUIState.ts
··· 14 14 foldedBlocks: [] as string[], 15 15 openPages: [] as string[], 16 16 selectedBlocks: [] as SelectedBlock[], 17 + openPopover: null as string | null, 17 18 }, 18 19 (set) => ({ 20 + setOpenPopover: (id: string | null) => { 21 + set({ openPopover: id }); 22 + }, 19 23 toggleFold: (entityID: string) => { 20 24 set((state) => { 21 25 return {
+63 -5
components/Blocks/ImageBlock.tsx
··· 1 1 "use client"; 2 2 3 3 import { useEntity, useReplicache } from "src/replicache"; 4 - import { Block, BlockProps } from "./Block"; 4 + import { BlockProps } from "./Block"; 5 5 import { useUIState } from "src/useUIState"; 6 6 import Image from "next/image"; 7 7 import { v7 } from "uuid"; ··· 9 9 import { generateKeyBetween } from "fractional-indexing"; 10 10 import { addImage, localImages } from "src/utils/addImage"; 11 11 import { elementId } from "src/utils/elementId"; 12 - import { useEffect } from "react"; 13 - import { deleteBlock } from "./DeleteBlock"; 12 + import { createContext, useContext, useEffect, useState } from "react"; 14 13 import { BlockImageSmall } from "components/Icons/BlockImageSmall"; 14 + import { Popover } from "components/Popover"; 15 + import { ImageAltSmall } from "components/Toolbar/ImageToolbar"; 16 + import { theme } from "tailwind.config"; 17 + import { EditTiny } from "components/Icons/EditTiny"; 18 + import { AsyncValueAutosizeTextarea } from "components/utils/AutosizeTextarea"; 19 + import { set } from "colorjs.io/fn"; 15 20 16 21 export function ImageBlock(props: BlockProps & { preview?: boolean }) { 17 22 let { rep } = useReplicache(); ··· 24 29 let isFullBleed = useEntity(props.value, "image/full-bleed")?.data.value; 25 30 let isFirst = props.previousBlock === null; 26 31 let isLast = props.nextBlock === null; 32 + 33 + let altText = useEntity(props.value, "image/alt")?.data.value; 27 34 28 35 let nextIsFullBleed = useEntity( 29 36 props.nextBlock && props.nextBlock.value, ··· 125 132 <img 126 133 loading="lazy" 127 134 decoding="async" 128 - alt={""} 135 + alt={altText} 129 136 src={isLocalUpload ? image.data.src + "?local" : image.data.fallback} 130 137 height={image?.data.height} 131 138 width={image?.data.width} 132 139 /> 133 140 ) : ( 134 141 <Image 135 - alt="" 142 + alt={altText || ""} 136 143 src={new URL(image.data.src).pathname.split("/").slice(5).join("/")} 137 144 height={image?.data.height} 138 145 width={image?.data.width} 139 146 className={className} 140 147 /> 141 148 )} 149 + {altText !== undefined ? <ImageAlt entityID={props.value} /> : null} 142 150 </div> 143 151 ); 144 152 } ··· 148 156 <div 149 157 className={`absolute top-3 sm:top-4 bottom-3 sm:bottom-4 left-3 sm:left-4 right-3 sm:right-4 border-2 border-bg-page rounded-lg outline-offset-1 outline outline-2 outline-tertiary`} 150 158 /> 159 + ); 160 + }; 161 + 162 + export const ImageBlockContext = createContext({ 163 + altEditorOpen: false, 164 + setAltEditorOpen: (s: boolean) => {}, 165 + }); 166 + 167 + const ImageAlt = (props: { entityID: string }) => { 168 + let { rep } = useReplicache(); 169 + let altText = useEntity(props.entityID, "image/alt")?.data.value; 170 + let entity_set = useEntitySetContext(); 171 + 172 + let setAltEditorOpen = useUIState((s) => s.setOpenPopover); 173 + let altEditorOpen = useUIState((s) => s.openPopover === props.entityID); 174 + 175 + if (!entity_set.permissions.write && altText === "") return null; 176 + return ( 177 + <div className="absolute bottom-0 right-2 h-max"> 178 + <Popover 179 + open={altEditorOpen} 180 + onOpenChange={(o) => setAltEditorOpen(o ? props.entityID : null)} 181 + className="text-sm max-w-xs min-w-0" 182 + side="left" 183 + trigger={<ImageAltSmall fillColor={theme.colors["bg-page"]} />} 184 + > 185 + {entity_set.permissions.write ? ( 186 + <AsyncValueAutosizeTextarea 187 + className="text-sm text-secondary outline-none bg-transparent min-w-0" 188 + value={altText} 189 + onFocus={(e) => { 190 + e.currentTarget.setSelectionRange( 191 + e.currentTarget.value.length, 192 + e.currentTarget.value.length, 193 + ); 194 + }} 195 + onChange={async (e) => { 196 + await rep?.mutate.assertFact({ 197 + entity: props.entityID, 198 + attribute: "image/alt", 199 + data: { type: "string", value: e.currentTarget.value }, 200 + }); 201 + }} 202 + placeholder="add alt text..." 203 + /> 204 + ) : ( 205 + <div className="text-sm text-secondary w-max"> {altText}</div> 206 + )} 207 + </Popover> 208 + </div> 151 209 ); 152 210 };
components/Toolbar/BlockToolbar.1.tsx
+5 -2
components/Toolbar/BlockToolbar.tsx
··· 6 6 import { useUIState } from "src/useUIState"; 7 7 import { LockBlockButton } from "./LockBlockButton"; 8 8 import { TextAlignmentButton } from "./TextAlignmentToolbar"; 9 - import { ImageFullBleedButton } from "./ImageToolbar"; 9 + import { ImageFullBleedButton, ImageAltTextButton } from "./ImageToolbar"; 10 10 import { DeleteSmall } from "components/Icons/DeleteSmall"; 11 11 12 12 export const BlockToolbar = (props: { 13 - setToolbarState: (state: "areYouSure" | "block" | "text-alignment") => void; 13 + setToolbarState: ( 14 + state: "areYouSure" | "block" | "text-alignment" | "img-alt-text", 15 + ) => void; 14 16 }) => { 15 17 let focusedEntity = useUIState((s) => s.focusedEntity); 16 18 let focusedEntityType = useEntity( ··· 41 43 <> 42 44 <TextAlignmentButton setToolbarState={props.setToolbarState} /> 43 45 <ImageFullBleedButton /> 46 + <ImageAltTextButton setToolbarState={props.setToolbarState} /> 44 47 {focusedEntityType?.data.value !== "canvas" && ( 45 48 <Separator classname="h-6" /> 46 49 )}
+90
components/Toolbar/ImageToolbar.tsx
··· 2 2 import { useEntity, useReplicache } from "src/replicache"; 3 3 import { useUIState } from "src/useUIState"; 4 4 import { Props } from "components/Icons/Props"; 5 + import { useContext, useEffect } from "react"; 6 + import { ImageBlockContext } from "components/Blocks/ImageBlock"; 7 + import { set } from "colorjs.io/fn"; 5 8 6 9 export const ImageFullBleedButton = (props: {}) => { 7 10 let { rep } = useReplicache(); ··· 25 28 tooltipContent={<div className="">Toggle Full Bleed</div>} 26 29 > 27 30 {isFullBleed ? <ImageFullBleedOnSmall /> : <ImageFullBleedOffSmall />} 31 + </ToolbarButton> 32 + ); 33 + }; 34 + 35 + export const ImageAltTextButton = (props: { 36 + setToolbarState: (s: "img-alt-text") => void; 37 + }) => { 38 + let { rep } = useReplicache(); 39 + let focusedBlock = useUIState((s) => s.focusedEntity)?.entityID || null; 40 + 41 + let altText = useEntity(focusedBlock, "image/alt")?.data.value; 42 + 43 + let setAltEditorOpen = useUIState((s) => s.setOpenPopover); 44 + let altEditorOpen = useUIState((s) => s.openPopover === focusedBlock); 45 + 46 + return ( 47 + <ToolbarButton 48 + active={altText !== undefined} 49 + onClick={async (e) => { 50 + e.preventDefault(); 51 + if (!focusedBlock) return; 52 + if (!altText) { 53 + await rep?.mutate.assertFact({ 54 + entity: focusedBlock, 55 + attribute: "image/alt", 56 + data: { type: "string", value: "" }, 57 + }); 58 + setAltEditorOpen(focusedBlock); 59 + } else { 60 + await rep?.mutate.retractAttribute({ 61 + entity: focusedBlock, 62 + attribute: "image/alt", 63 + }); 64 + setAltEditorOpen(null); 65 + } 66 + }} 67 + tooltipContent={ 68 + <div>{altText === undefined ? "Add " : "Remove "}Alt Text</div> 69 + } 70 + > 71 + {altText === undefined ? ( 72 + <ImageAltSmall fillColor="transparent" /> 73 + ) : ( 74 + <ImageRemoveAltSmall /> 75 + )} 28 76 </ToolbarButton> 29 77 ); 30 78 }; ··· 62 110 fillRule="evenodd" 63 111 clipRule="evenodd" 64 112 d="M0.712524 5.68921C0.712524 3.96332 2.11163 2.56421 3.83752 2.56421H20.1624C21.8883 2.56421 23.2874 3.96332 23.2874 5.68921V18.3108C23.2874 20.0367 21.8883 21.4358 20.1624 21.4358H3.83752C2.11164 21.4358 0.712524 20.0367 0.712524 18.3108V5.68921ZM3.83752 3.81421C2.80199 3.81421 1.96252 4.65368 1.96252 5.68921V14.1901L4.82906 11.6316C5.21149 11.2902 5.7763 11.2506 6.20267 11.5351L10.2892 14.2624L16.3336 7.98778C16.573 7.73919 16.9687 7.73179 17.2173 7.97126C17.4659 8.21074 17.4733 8.6064 17.2338 8.85499L13.5626 12.6661L14.0542 13.0054C14.1688 13.0845 14.3178 13.0936 14.4412 13.0289L15.5068 12.4708C15.8425 12.2949 16.2291 12.242 16.5997 12.321L18.8672 12.8047C18.9866 12.8301 19.1109 12.7959 19.2004 12.7129L20.5274 11.4827C20.4554 11.282 20.4879 11.0496 20.6334 10.8732C20.8531 10.607 21.247 10.5692 21.5133 10.7889L22.0374 11.2213V5.68921C22.0374 4.65368 21.198 3.81421 20.1624 3.81421H3.83752ZM21.4356 12.3453L20.0502 13.6296C19.6624 13.9892 19.1237 14.1375 18.6065 14.0272L16.339 13.5435C16.2535 13.5253 16.1642 13.5375 16.0868 13.5781L15.0212 14.1362C14.4866 14.4163 13.8408 14.377 13.3441 14.0341L12.6838 13.5783L11.3433 14.9699L11.6378 15.1684C11.9241 15.3613 11.9997 15.7498 11.8068 16.036C11.6139 16.3222 11.2255 16.3979 10.9392 16.205L10.0299 15.5921L10.0276 15.5905L9.07582 14.9554L7.84586 16.0297C7.58589 16.2568 7.19106 16.2301 6.96399 15.9702C6.73691 15.7102 6.76357 15.3154 7.02354 15.0883L7.99868 14.2365L5.58925 12.6286L2.3289 15.5385C2.22302 15.6331 2.09388 15.6849 1.96252 15.6953V18.3108C1.96252 19.3463 2.80199 20.1858 3.83752 20.1858H20.1624C21.198 20.1858 22.0374 19.3463 22.0374 18.3108V12.8418L21.4356 12.3453ZM9.71952 6.20252C9.39162 6.0947 9.03839 6.27312 8.93057 6.60102C8.82275 6.92893 9.00116 7.28216 9.32907 7.38998C9.67978 7.5053 9.84306 7.66604 9.92838 7.80233C10.02 7.94875 10.0598 8.12899 10.0598 8.33053C10.0598 8.79991 9.67925 9.18042 9.20987 9.18042C9.06221 9.18042 8.79851 9.10649 8.58819 8.92328C8.40267 8.76167 8.25172 8.51136 8.28005 8.10042C8.3038 7.75606 8.04389 7.45765 7.69953 7.4339C7.35517 7.41015 7.05676 7.67006 7.03302 8.01442C6.97619 8.83849 7.30377 9.46218 7.76714 9.86582C8.20571 10.2479 8.76126 10.4304 9.20987 10.4304C10.3696 10.4304 11.3098 9.49027 11.3098 8.33053C11.3098 7.97485 11.2407 7.54299 10.9879 7.13909C10.7287 6.72507 10.3118 6.39728 9.71952 6.20252ZM16.1204 5.922C16.1204 5.50778 16.4561 5.172 16.8704 5.172H19.3704C20.0607 5.172 20.6204 5.73164 20.6204 6.422V8.922C20.6204 9.33621 20.2846 9.672 19.8704 9.672C19.4561 9.672 19.1204 9.33621 19.1204 8.922V6.672H16.8704C16.4561 6.672 16.1204 6.33621 16.1204 5.922ZM16.8704 18.8224C16.4561 18.8224 16.1204 18.4866 16.1204 18.0724C16.1204 17.6582 16.4561 17.3224 16.8704 17.3224H19.1204V15.0724C19.1204 14.6582 19.4561 14.3224 19.8704 14.3224C20.2846 14.3224 20.6204 14.6582 20.6204 15.0724V17.5724C20.6204 18.2627 20.0607 18.8224 19.3704 18.8224H16.8704ZM7.84949 5.9248C7.84949 5.51059 7.5137 5.1748 7.09949 5.1748H4.59949C3.90913 5.1748 3.34949 5.73445 3.34949 6.4248V8.9248C3.34949 9.33902 3.68527 9.6748 4.09949 9.6748C4.5137 9.6748 4.84949 9.33902 4.84949 8.9248V6.6748H7.09949C7.5137 6.6748 7.84949 6.33902 7.84949 5.9248ZM7.09949 18.8252C7.5137 18.8252 7.84949 18.4894 7.84949 18.0752C7.84949 17.661 7.5137 17.3252 7.09949 17.3252H4.84949V15.0752C4.84949 14.661 4.5137 14.3252 4.09949 14.3252C3.68527 14.3252 3.34949 14.661 3.34949 15.0752V17.5752C3.34949 18.2656 3.90913 18.8252 4.59949 18.8252H7.09949Z" 113 + fill="currentColor" 114 + /> 115 + </svg> 116 + ); 117 + }; 118 + 119 + export const ImageAltSmall = (props: { fillColor: string }) => { 120 + return ( 121 + <svg 122 + width="24" 123 + height="24" 124 + viewBox="0 0 24 24" 125 + fill="none" 126 + xmlns="http://www.w3.org/2000/svg" 127 + > 128 + <rect 129 + x="1.07886" 130 + y="5.237" 131 + width="21.8423" 132 + height="13.4703" 133 + rx="2.5" 134 + fill={props.fillColor} 135 + /> 136 + <path 137 + d="M21.0718 5C22.3323 5.12817 23.3159 6.19299 23.3159 7.4873V16.4443L23.3032 16.7002C23.1837 17.8766 22.2482 18.812 21.0718 18.9316L20.8159 18.9443H3.89014L2.92822 18.9316C1.752 18.8118 0.816287 17.8765 0.696777 16.7002L0.684082 7.4873C0.684082 6.19312 1.66793 5.12835 2.92822 5H21.0718ZM3.18408 6.2373C2.49395 6.23756 1.93408 6.7971 1.93408 7.4873V16.4443C1.93408 17.1345 2.49395 17.6941 3.18408 17.6943H20.8159C21.5062 17.6943 22.0659 17.1347 22.0659 16.4443V7.4873C22.0659 6.79698 21.5062 6.23736 20.8159 6.2373H3.18408ZM6.7876 8.3291C7.10561 8.32913 7.38885 8.53018 7.49463 8.83008L9.57275 14.7236C9.7236 15.1525 9.40533 15.6023 8.95068 15.6025C8.66735 15.6025 8.41495 15.4209 8.32471 15.1523L7.87354 13.8086H5.13916L4.68896 15.1523C4.5988 15.4211 4.34649 15.6025 4.06299 15.6025C3.6083 15.6023 3.29009 15.1526 3.44092 14.7236L5.51514 8.83008C5.62084 8.53004 5.90502 8.32913 6.22314 8.3291H6.7876ZM11.3452 8.3291C11.7089 8.32928 12.0034 8.62458 12.0034 8.98828V14.1777C12.0035 14.2329 12.0489 14.2773 12.104 14.2773H14.5444C14.9101 14.2774 15.2063 14.5739 15.2065 14.9395C15.2065 15.3052 14.9102 15.6025 14.5444 15.6025H11.686C11.1339 15.6025 10.6862 15.1546 10.686 14.6025V8.98828C10.686 8.62449 10.9814 8.32912 11.3452 8.3291ZM19.7026 8.3291C20.0076 8.3291 20.2554 8.57686 20.2554 8.88184C20.2553 9.18673 20.0076 9.43359 19.7026 9.43359H18.1079C18.0527 9.4336 18.0073 9.47895 18.0073 9.53418V14.9492C18.0071 15.3099 17.7147 15.6025 17.354 15.6025C16.9933 15.6025 16.7009 15.3099 16.7007 14.9492V9.53418C16.7007 9.4791 16.6561 9.43384 16.6011 9.43359H15.0054C14.7005 9.43358 14.4527 9.18672 14.4526 8.88184C14.4526 8.57687 14.7004 8.32911 15.0054 8.3291H19.7026ZM6.50537 9.82129C6.48845 9.82132 6.4727 9.83163 6.46729 9.84766L5.49365 12.751H7.51807L6.54248 9.84766C6.53703 9.83177 6.52217 9.82138 6.50537 9.82129Z" 138 + fill="currentColor" 139 + /> 140 + </svg> 141 + ); 142 + }; 143 + 144 + const ImageRemoveAltSmall = () => { 145 + return ( 146 + <svg 147 + width="24" 148 + height="24" 149 + viewBox="0 0 24 24" 150 + fill="none" 151 + xmlns="http://www.w3.org/2000/svg" 152 + > 153 + <path 154 + d="M19.7529 3.18659C20.0459 2.89419 20.5207 2.89386 20.8135 3.18659C21.1061 3.47932 21.1058 3.95423 20.8135 4.24713L4.24805 20.8126C3.95517 21.1054 3.48038 21.1053 3.1875 20.8126C2.89463 20.5197 2.89467 20.0449 3.1875 19.752L19.7529 3.18659ZM21.6631 5.1651C22.6263 5.51251 23.3164 6.43272 23.3164 7.51568V16.4727L23.3037 16.7286C23.184 17.9046 22.2483 18.8402 21.0723 18.96L20.8164 18.9727H7.85645L9.10645 17.7227H20.8164C21.5063 17.7224 22.0661 17.1626 22.0664 16.4727V7.51568C22.0664 6.82552 21.5065 6.266 20.8164 6.26569H20.5635L21.6631 5.1651ZM14.9072 6.26569H3.18457C2.49444 6.26594 1.93457 6.82548 1.93457 7.51568V16.4727C1.93488 17.1626 2.49463 17.7225 3.18457 17.7227H3.4502L2.3457 18.8262C1.45485 18.5088 0.796417 17.7021 0.697266 16.7286L0.683594 7.51568C0.683594 6.2215 1.66842 5.15576 2.92871 5.0274H16.1455L14.9072 6.26569ZM6.78809 8.35748C7.1059 8.35772 7.38939 8.55871 7.49512 8.85846L8.75098 12.421L7.33594 13.837H5.13965L4.68945 15.1807C4.59924 15.4491 4.34665 15.6307 4.06348 15.6309C3.60892 15.6307 3.29088 15.1809 3.44141 14.752L5.51562 8.85846C5.62133 8.55842 5.90551 8.35751 6.22363 8.35748H6.78809ZM14.5449 14.3057C14.9103 14.306 15.2068 14.6024 15.207 14.9678C15.2067 15.3332 14.9103 15.6306 14.5449 15.6309H11.6865C11.5431 15.6309 11.4065 15.5994 11.2832 15.545L12.5234 14.3057H14.5449ZM19.7031 8.35748C20.0079 8.35775 20.2559 8.60541 20.2559 8.91022C20.2555 9.21468 20.0076 9.46171 19.7031 9.46197H18.1084C18.0532 9.46198 18.0078 9.50733 18.0078 9.56256V14.9776C18.0073 15.3378 17.7148 15.6307 17.3545 15.6309C16.994 15.6309 16.7017 15.338 16.7012 14.9776V10.128L18.4717 8.35748H19.7031ZM6.50586 9.84967C6.48894 9.84969 6.47319 9.86 6.46777 9.87603L5.49414 12.7794H7.51855L6.54297 9.87603C6.53757 9.86031 6.52245 9.84996 6.50586 9.84967ZM11.3457 8.35748C11.7091 8.35793 12.0039 8.65313 12.0039 9.01666V9.169L10.6865 10.4864V9.01666C10.6865 8.65286 10.9819 8.3575 11.3457 8.35748Z" 65 155 fill="currentColor" 66 156 /> 67 157 </svg>
+4
src/replicache/attributes.ts
··· 153 153 type: "boolean", 154 154 cardinality: "one", 155 155 }, 156 + "image/alt": { 157 + type: "string", 158 + cardinality: "one", 159 + }, 156 160 } as const; 157 161 158 162 const PollBlockAttributes = {
+27 -7
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 1 + "use client"; 1 2 import { 2 3 PubLeafletBlocksHeader, 3 4 PubLeafletBlocksImage, ··· 9 10 } from "lexicons/api"; 10 11 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 11 12 import { TextBlock } from "./TextBlock"; 13 + import { Popover } from "components/Popover"; 14 + import { ImageAltSmall } from "components/Toolbar/ImageToolbar"; 15 + import { theme } from "tailwind.config"; 12 16 13 17 export function PostContent({ 14 18 blocks, ··· 100 104 </div> 101 105 {b.block.previewImage && ( 102 106 <div 103 - className={`linkBlockPreview w-[120px] m-2 -mb-2 bg-cover shrink-0 rounded-t-md border border-border rotate-[4deg] origin-center`} 107 + className={`imagePreview w-[120px] m-2 -mb-2 bg-cover shrink-0 rounded-t-md border border-border rotate-[4deg] origin-center relative`} 104 108 style={{ 105 109 backgroundImage: `url(${blobRefToSrc(b.block.previewImage?.ref, did)})`, 106 110 backgroundPosition: "center", ··· 112 116 } 113 117 case PubLeafletBlocksImage.isMain(b.block): { 114 118 return ( 115 - <img 116 - height={b.block.aspectRatio?.height} 117 - width={b.block.aspectRatio?.width} 118 - className={`!pt-3 sm:!pt-4 ${className}`} 119 - src={blobRefToSrc(b.block.image.ref, did)} 120 - /> 119 + <div className="relative"> 120 + <img 121 + alt={b.block.alt} 122 + height={b.block.aspectRatio?.height} 123 + width={b.block.aspectRatio?.width} 124 + className={`!pt-3 sm:!pt-4 ${className} rounded-md`} 125 + src={blobRefToSrc(b.block.image.ref, did)} 126 + /> 127 + {b.block.alt && ( 128 + <div className="absolute bottom-1.5 right-2 h-max"> 129 + <Popover 130 + className="text-sm max-w-xs min-w-0" 131 + side="left" 132 + trigger={<ImageAltSmall fillColor={theme.colors["bg-page"]} />} 133 + > 134 + <div className="text-sm text-secondary w-full"> 135 + {b.block.alt} 136 + </div> 137 + </Popover> 138 + </div> 139 + )} 140 + </div> 121 141 ); 122 142 } 123 143 case PubLeafletBlocksText.isMain(b.block):