diff --git a/src/web/stepper/src/SubstVisualizer.tsx b/src/web/stepper/src/SubstVisualizer.tsx
index 8b81e24..694f9c5 100644
--- a/src/web/stepper/src/SubstVisualizer.tsx
+++ b/src/web/stepper/src/SubstVisualizer.tsx
@@ -397,6 +397,23 @@ function ProfileHoverTextPopover({ text }: { text: string }) {
);
}
+/** Popover body for an `image` part: the inline opaque-value thumbnail, enlarged to be legible. */
+function EnlargedThumbnailPopover({ src, alt }: { src: string; alt?: string }) {
+ return (
+
+
+ {alt ? (
+ <>
+
+
{` ${alt}`}
+ >
+ ) : null}
+

+
+
+ );
+}
+
/**
* renderNode renders a serialized Stepper AST node to a React ReactNode.
*/
@@ -860,14 +877,23 @@ function renderNode(
const src = readNodeProp(node, part.image);
if (typeof src !== "string" || !src.startsWith("data:")) return null;
const alt = part.altProp === undefined ? undefined : readNodeProp(node, part.altProp);
+ const altText = alt == null ? "" : String(alt);
return (
-
+ interactionKind="hover"
+ placement="bottom"
+ usePortal={popoverDepth === 0}
+ lazy
+ popoverClassName="stepper-popover"
+ content={}
+ >
+
+
);
}
return null;
diff --git a/src/web/stepper/src/styles.ts b/src/web/stepper/src/styles.ts
index 801e73f..52f495e 100644
--- a/src/web/stepper/src/styles.ts
+++ b/src/web/stepper/src/styles.ts
@@ -62,6 +62,19 @@ const STEPPER_CSS = `
border-radius: 3px;
}
+/* The same thumbnail enlarged, shown in the hover popover so an opaque value (e.g. a rune) is
+ * actually legible while the inline copy stays at text height. A fixed width (not max-width) forces
+ * a low-resolution thumbnail to scale up rather than render at its small intrinsic size. */
+.sa-substituter .stepper-opaque-thumbnail-large,
+.stepper-popover .stepper-opaque-thumbnail-large {
+ display: block;
+ width: min(200px, 50vw);
+ height: auto;
+ margin-top: 6px;
+ border-radius: 3px;
+ image-rendering: auto;
+}
+
.sa-substituter .stepper-mu-term,
.stepper-popover .stepper-mu-term {
font-weight: bold;