From 8054016cc9d6f58534cc7601edea5341693e2490 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Thu, 20 Aug 2026 09:15:07 +0000
Subject: [PATCH 01/10] feat: overlay persisted image-region boxes on source
figures
Draw buyer-facing region locators from stored x/y/width/height ratios
when the source image can be reattached. Invalid boxes and missing
bitmaps stay list-only. Never invent coordinates or show LLM
instructions.
ADR 0111, v2.13.2.
---
AGENTS.md | 8 +-
CHANGELOG.d/2.13.2-image-region-overlays.md | 15 +++
.../0110-buyer-image-evidence-rendering.md | 2 +-
docs/adr/0111-buyer-image-region-overlays.md | 32 ++++++
frontend/package.json | 2 +-
frontend/src/App.css | 50 ++++++++++
frontend/src/PostBody.test.tsx | 77 +++++++++++++++
frontend/src/PostBody.tsx | 99 +++++++++++++++----
frontend/src/i18n.test.ts | 3 +
frontend/src/i18n.ts | 8 ++
lineageweave/__init__.py | 2 +-
pyproject.toml | 2 +-
uv.lock | 2 +-
13 files changed, 278 insertions(+), 24 deletions(-)
create mode 100644 CHANGELOG.d/2.13.2-image-region-overlays.md
create mode 100644 docs/adr/0111-buyer-image-region-overlays.md
diff --git a/AGENTS.md b/AGENTS.md
index aa93be63e..6688e2e5b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -127,7 +127,10 @@ contextual-orchestrator owns model discovery and selection.
retaining the original asset and provenance. Recognize image DOM/visual
regions before OCR, descriptions, Keyman extraction, or embeddings. Store
region-level evidence; never show an internal LLM instruction such as
- `This post is an image` to a buyer.
+ `This post is an image` to a buyer. When a source image can be reattached,
+ draw only persisted visual-region boxes; never invent coordinates. If the
+ bitmap is missing or a box is invalid, keep the region list and do not
+ fabricate a locator (ADR 0110 / 0111).
## Source parsing and semantic units
@@ -146,7 +149,8 @@ contextual-orchestrator owns model discovery and selection.
paragraphs, not the authoring application's spacing workaround.
- Image descriptions, OCR text, and region evidence are analysis artifacts,
not buyer-facing prompt instructions. Buyer UI shows the source content and
- useful captions/evidence only, with provenance where appropriate.
+ useful captions/evidence only, with provenance where appropriate. Overlay
+ boxes use stored ratios; a missing image fails closed to the list.
## Pluggable channels: never fake a missing signal
diff --git a/CHANGELOG.d/2.13.2-image-region-overlays.md b/CHANGELOG.d/2.13.2-image-region-overlays.md
new file mode 100644
index 000000000..a8aa057da
--- /dev/null
+++ b/CHANGELOG.d/2.13.2-image-region-overlays.md
@@ -0,0 +1,15 @@
+# 2.13.2 — Overlay persisted image-region boxes
+
+## Added
+
+- Buyer post figures now draw persisted visual-region boxes on a reattached
+ source image. Selecting a box announces that region's caption or OCR.
+- Invalid, missing, or out-of-bounds coordinates stay list-only. When the
+ bitmap cannot be reattached, the region list remains the fallback and no
+ locator is invented.
+
+## Changed
+
+- Image-region overlay and current-region labels are translated through the
+ five-locale catalog (Korean, English fallback, Chinese, Japanese,
+ Vietnamese).
diff --git a/docs/adr/0110-buyer-image-evidence-rendering.md b/docs/adr/0110-buyer-image-evidence-rendering.md
index d9f8e457b..803d75939 100644
--- a/docs/adr/0110-buyer-image-evidence-rendering.md
+++ b/docs/adr/0110-buyer-image-evidence-rendering.md
@@ -1,4 +1,4 @@
-# ADR 0104: Render image evidence as buyer content, not LLM instructions
+# ADR 0110: Render image evidence as buyer content, not LLM instructions
- Status: Accepted
- Date: 2026-08-20
diff --git a/docs/adr/0111-buyer-image-region-overlays.md b/docs/adr/0111-buyer-image-region-overlays.md
new file mode 100644
index 000000000..60c42e212
--- /dev/null
+++ b/docs/adr/0111-buyer-image-region-overlays.md
@@ -0,0 +1,32 @@
+# ADR 0111: Overlay persisted visual-region boxes on the source image
+
+- Status: Accepted
+- Date: 2026-08-20
+- Depends on: [0067](0067-visual-region-vision-agent.md), [0091](0091-visual-region-embedding-persistence.md), [0110](0110-buyer-image-evidence-rendering.md)
+
+## Context
+
+ADR 0067 and ADR 0091 persist parent-relative `x_ratio`, `y_ratio`,
+`width_ratio`, and `height_ratio` for each accepted visual region. ADR 0110
+renders captions, OCR, tags, and a region list, but the buyer still cannot
+see which panel of the source image supplied that evidence.
+
+## Decision
+
+- When the source image can be reattached and a region has a finite persisted
+ box inside the unit square, draw that box on the figure as a
+ keyboard-focusable control. Selecting it announces the persisted caption or
+ OCR as the current image region.
+- Never invent coordinates. Invalid, missing, non-finite, zero-area, or
+ out-of-bounds boxes stay list-only. When the source image cannot be
+ reattached, keep the region list and do not draw empty overlays.
+- Do not surface LLM instructions from image `unit_text`. Overlay labels come
+ only from persisted region caption or extracted text.
+- Translate overlay and current-region labels through the five-locale UI
+ catalog.
+
+## Consequences
+
+Buyers can locate panel-level evidence on the source image. Search and
+embedding remain bound to the existing region tables. A missing bitmap still
+fails closed to the list rather than fabricating a locator.
diff --git a/frontend/package.json b/frontend/package.json
index e2e996bbe..a8d0f7b80 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
- "version": "2.12.6",
+ "version": "2.13.2",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 795ed2a3b..9f905f0e6 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -349,6 +349,56 @@
height: auto;
}
+.post-embedded-image-frame {
+ position: relative;
+ display: inline-block;
+ max-width: 100%;
+}
+
+.post-embedded-image-frame img {
+ display: block;
+ max-width: 100%;
+ height: auto;
+}
+
+.post-image-region-overlays {
+ position: absolute;
+ inset: 0;
+}
+
+.post-image-region-overlay {
+ position: absolute;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ border: 2px solid var(--color-accent-info);
+ background: var(--color-accent-info-background);
+ cursor: pointer;
+ appearance: none;
+}
+
+.post-image-region-overlay[aria-pressed="true"] {
+ border-color: var(--color-accent);
+ background: var(--color-accent-background);
+}
+
+.post-image-region-overlay:focus-visible {
+ outline: 2px solid var(--color-accent);
+ outline-offset: 2px;
+}
+
+.post-image-current-region {
+ margin: 0.4rem 0 0;
+ font-size: 0.85rem;
+ color: var(--text-h);
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .post-image-region-overlay {
+ transition: border-color 160ms ease, background-color 160ms ease;
+ }
+}
+
@media print {
body * {
visibility: hidden;
diff --git a/frontend/src/PostBody.test.tsx b/frontend/src/PostBody.test.tsx
index 820a120a3..36ee7bdc7 100644
--- a/frontend/src/PostBody.test.tsx
+++ b/frontend/src/PostBody.test.tsx
@@ -1,4 +1,5 @@
import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { PostBody } from "./PostBody";
@@ -188,5 +189,81 @@ describe("PostBody", () => {
expect(screen.getByText("diagram, process")).toBeInTheDocument();
expect(screen.getByText("Main panel")).toBeInTheDocument();
expect(screen.queryByText(/This post is an image/)).not.toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: /Main panel/ })).not.toBeInTheDocument();
+ });
+
+ it("overlays persisted region boxes on a reattached source image", async () => {
+ const user = userEvent.setup();
+ const source =
+ '
';
+
+ render(
+ ,
+ );
+
+ const overlay = screen.getByRole("button", { name: "Image region: Main panel" });
+ expect(overlay).toHaveStyle({
+ left: "10%",
+ top: "20%",
+ width: "30%",
+ height: "40%",
+ });
+ expect(screen.queryByRole("button", { name: /Overflow panel/ })).not.toBeInTheDocument();
+ expect(screen.queryByText(/This post is an image/)).not.toBeInTheDocument();
+ expect(screen.queryByText(/Invented box must not render/)).not.toBeInTheDocument();
+ expect(screen.getByText("Overflow panel")).toBeInTheDocument();
+
+ overlay.focus();
+ expect(overlay).toHaveFocus();
+ await user.click(overlay);
+ expect(screen.getByText("Current image region: Main panel")).toBeInTheDocument();
});
});
diff --git a/frontend/src/PostBody.tsx b/frontend/src/PostBody.tsx
index 338951b3c..5fc14fde8 100644
--- a/frontend/src/PostBody.tsx
+++ b/frontend/src/PostBody.tsx
@@ -1,21 +1,79 @@
+import { useState, type ReactNode } from "react";
import { splitPostBody, type PostBodySegment } from "./postBodyDisplay";
import { t } from "./i18n";
-import type { PostContentUnit, PostImageContent } from "./api";
-import type { ReactNode } from "react";
+import type { PostContentUnit, PostImageContent, PostImageRegion } from "./api";
+
+function hasPersistedOverlayBox(region: PostImageRegion): boolean {
+ const { x_ratio, y_ratio, width_ratio, height_ratio } = region;
+ if (![x_ratio, y_ratio, width_ratio, height_ratio].every((value) => Number.isFinite(value))) {
+ return false;
+ }
+ if (x_ratio < 0 || y_ratio < 0 || width_ratio <= 0 || height_ratio <= 0) {
+ return false;
+ }
+ return x_ratio + width_ratio <= 1 && y_ratio + height_ratio <= 1;
+}
+
+function regionBuyerLabel(region: PostImageRegion): string {
+ return region.caption || region.extracted_text || t("Unknown");
+}
+
+function ImageEvidenceFigure({
+ imageContent,
+ sourceImage,
+}: {
+ imageContent?: PostImageContent;
+ sourceImage?: Extract;
+}) {
+ const [selectedRegionIndex, setSelectedRegionIndex] = useState(null);
+ const regions = imageContent?.regions ?? [];
+ const overlayRegions = sourceImage ? regions.filter(hasPersistedOverlayBox) : [];
+ const selectedRegion = overlayRegions.find((region) => region.region_index === selectedRegionIndex);
-function renderImageEvidence(
- index: number,
- imageContent?: PostImageContent,
- sourceImage?: Extract,
-) {
return (
-
+
{sourceImage ? (
-
+
+

+ {overlayRegions.length ? (
+
+ {overlayRegions.map((region) => {
+ const label = regionBuyerLabel(region);
+ const pressed = selectedRegionIndex === region.region_index;
+ return (
+
+ ) : null}
+
) : null}
{imageContent?.caption || !sourceImage ? (
{imageContent?.caption || t("Embedded image")}
) : null}
+ {selectedRegion ? (
+
+ {t("Current image region")}: {regionBuyerLabel(selectedRegion)}
+
+ ) : null}
{imageContent?.tags.length ? (
{t("Image tags")}: {imageContent.tags.join(", ")}
@@ -27,13 +85,13 @@ function renderImageEvidence(
{imageContent.extracted_text}
) : null}
- {imageContent?.regions?.length ? (
+ {regions.length ? (
{t("Image regions")}
- {imageContent.regions.map((region) => (
+ {regions.map((region) => (
-
- {region.caption || region.extracted_text || t("Unknown")}
+ {regionBuyerLabel(region)}
{region.tags.length ? (
{t("Image tags")}: {region.tags.join(", ")}
@@ -67,7 +125,13 @@ function renderSegment(segment: PostBodySegment, index: number, imageContent?: P
);
case "image":
- return renderImageEvidence(index, imageContent, segment);
+ return (
+
+ );
default: {
const _exhaustive: never = segment;
throw new Error(`unexpected post body segment: ${JSON.stringify(_exhaustive)}`);
@@ -104,9 +168,11 @@ function renderStructuredUnits(
const sourceImage = sourceImages[imageOrdinal++];
const content = imageContent.find((item) => item.unit_index === unit.unit_index);
rendered.push(
- sourceImage
- ? renderSegment(sourceImage, index, content)
- : renderImageEvidence(index, content),
+ sourceImage ? (
+ renderSegment(sourceImage, index, content)
+ ) : (
+
+ ),
);
index += 1;
continue;
@@ -176,7 +242,6 @@ export function PostBody({
{splitPostBody(body).map((segment, index) => {
const content = segment.kind === "image" ? imageContent[imageOrdinal++] : undefined;
- if (segment.kind !== "text") return renderSegment(segment, index, content);
return renderSegment(segment, index, content);
})}
diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts
index 5a3afbfc3..77eb6b75c 100644
--- a/frontend/src/i18n.test.ts
+++ b/frontend/src/i18n.test.ts
@@ -23,6 +23,9 @@ describe("i18n", () => {
"Keymen",
"Unknown",
"Image tags",
+ "Image regions",
+ "Image region",
+ "Current image region",
"Counterparties",
"due",
"Activity",
diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts
index d5717f396..b389ba566 100644
--- a/frontend/src/i18n.ts
+++ b/frontend/src/i18n.ts
@@ -273,6 +273,8 @@ const TRANSLATIONS: Partial>> = {
"Embedded image": "삽입 이미지",
"Text detected in image": "이미지에서 인식된 텍스트",
"Image regions": "이미지 영역",
+ "Image region": "이미지 영역",
+ "Current image region": "현재 이미지 영역",
"Image tags": "이미지 태그",
"Embedded image could not be decoded. Re-export the source post and open it again.":
"첨부 이미지를 해독할 수 없습니다. 원문을 다시 내보내고 다시 여세요.",
@@ -603,6 +605,8 @@ const TRANSLATIONS: Partial>> = {
"Embedded image": "嵌入图像",
"Text detected in image": "图像中识别的文字",
"Image regions": "图像区域",
+ "Image region": "图像区域",
+ "Current image region": "当前图像区域",
"Image tags": "图像标签",
"Embedded image could not be decoded. Re-export the source post and open it again.":
"无法解码嵌入图像。请重新导出原始文章后再打开。",
@@ -948,6 +952,8 @@ const TRANSLATIONS: Partial>> = {
"Embedded image": "埋め込み画像",
"Text detected in image": "画像から認識されたテキスト",
"Image regions": "画像領域",
+ "Image region": "画像領域",
+ "Current image region": "現在の画像領域",
"Image tags": "画像タグ",
"Embedded image could not be decoded. Re-export the source post and open it again.":
"埋め込み画像をデコードできませんでした。原文を再エクスポートして、もう一度開いてください。",
@@ -1278,6 +1284,8 @@ const TRANSLATIONS: Partial>> = {
"Embedded image": "Hình ảnh nhúng",
"Text detected in image": "Văn bản nhận dạng trong hình ảnh",
"Image regions": "Các vùng trong hình ảnh",
+ "Image region": "Vùng hình ảnh",
+ "Current image region": "Vùng hình ảnh hiện tại",
"Image tags": "Thẻ hình ảnh",
"Embedded image could not be decoded. Re-export the source post and open it again.":
"Không thể giải mã hình ảnh nhúng. Hãy xuất lại bài viết gốc rồi mở lại.",
diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py
index 95330cb50..2e052a26d 100644
--- a/lineageweave/__init__.py
+++ b/lineageweave/__init__.py
@@ -55,4 +55,4 @@
"sentence_excerpts",
]
-__version__ = "2.12.6"
+__version__ = "2.13.2"
diff --git a/pyproject.toml b/pyproject.toml
index cb4be2916..cd3acca1f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
-version = "2.12.6"
+version = "2.13.2"
description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication."
readme = "README.md"
license = { text = "MIT" }
diff --git a/uv.lock b/uv.lock
index 10bcf9ff1..de224e53a 100644
--- a/uv.lock
+++ b/uv.lock
@@ -454,7 +454,7 @@ wheels = [
[[package]]
name = "lineageweave"
-version = "2.12.6"
+version = "2.13.2"
source = { editable = "." }
dependencies = [
{ name = "certifi" },
From e7d1b353bb423c6bceb4664f96227cda3a987a29 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Thu, 20 Aug 2026 18:31:05 +0900
Subject: [PATCH 02/10] docs: avoid image overlay ADR collision
---
AGENTS.md | 2 +-
...e-region-overlays.md => 0112-buyer-image-region-overlays.md} | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
rename docs/adr/{0111-buyer-image-region-overlays.md => 0112-buyer-image-region-overlays.md} (95%)
diff --git a/AGENTS.md b/AGENTS.md
index 6688e2e5b..7431440f5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -130,7 +130,7 @@ contextual-orchestrator owns model discovery and selection.
`This post is an image` to a buyer. When a source image can be reattached,
draw only persisted visual-region boxes; never invent coordinates. If the
bitmap is missing or a box is invalid, keep the region list and do not
- fabricate a locator (ADR 0110 / 0111).
+ fabricate a locator (ADR 0110 / 0112).
## Source parsing and semantic units
diff --git a/docs/adr/0111-buyer-image-region-overlays.md b/docs/adr/0112-buyer-image-region-overlays.md
similarity index 95%
rename from docs/adr/0111-buyer-image-region-overlays.md
rename to docs/adr/0112-buyer-image-region-overlays.md
index 60c42e212..b101aa0d1 100644
--- a/docs/adr/0111-buyer-image-region-overlays.md
+++ b/docs/adr/0112-buyer-image-region-overlays.md
@@ -1,4 +1,4 @@
-# ADR 0111: Overlay persisted visual-region boxes on the source image
+# ADR 0112: Overlay persisted visual-region boxes on the source image
- Status: Accepted
- Date: 2026-08-20
From 668168859a99e218668c8413881c3b6bcf7121d4 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Thu, 20 Aug 2026 18:40:29 +0900
Subject: [PATCH 03/10] test: cover invalid image overlay bounds
---
frontend/src/PostBody.test.tsx | 84 ++++++++++++++++++++++++++++++++++
1 file changed, 84 insertions(+)
diff --git a/frontend/src/PostBody.test.tsx b/frontend/src/PostBody.test.tsx
index 36ee7bdc7..cd2fc5bae 100644
--- a/frontend/src/PostBody.test.tsx
+++ b/frontend/src/PostBody.test.tsx
@@ -266,4 +266,88 @@ describe("PostBody", () => {
await user.click(overlay);
expect(screen.getByText("Current image region: Main panel")).toBeInTheDocument();
});
+
+ it("keeps invalid persisted boxes list-only and does not create overlay controls", () => {
+ render(
+ '}
+ structureUnits={[
+ {
+ unit_index: 0,
+ unit_kind_code: "image",
+ unit_label: "img",
+ unit_text: "internal image instruction",
+ indent_level: 0,
+ indent_source_code: "unresolved",
+ indent_confidence: 0,
+ indent_evidence: "",
+ },
+ ]}
+ imageContent={[
+ {
+ unit_index: 0,
+ mime_type: "image/png",
+ status_code: "described",
+ extracted_text: "",
+ caption: "",
+ tags: [],
+ regions: [
+ {
+ region_index: 0,
+ x_ratio: Number.NaN,
+ y_ratio: 0,
+ width_ratio: 0.2,
+ height_ratio: 0.2,
+ status_code: "unavailable",
+ extracted_text: "NaN box",
+ caption: "NaN box",
+ tags: [],
+ },
+ {
+ region_index: 1,
+ x_ratio: -0.1,
+ y_ratio: 0,
+ width_ratio: 0.2,
+ height_ratio: 0.2,
+ status_code: "unavailable",
+ extracted_text: "Negative box",
+ caption: "Negative box",
+ tags: [],
+ },
+ {
+ region_index: 2,
+ x_ratio: 0,
+ y_ratio: 0,
+ width_ratio: 0,
+ height_ratio: 0.2,
+ status_code: "unavailable",
+ extracted_text: "Zero box",
+ caption: "Zero box",
+ tags: [],
+ },
+ {
+ region_index: 3,
+ x_ratio: 0.8,
+ y_ratio: 0.8,
+ width_ratio: 0.3,
+ height_ratio: 0.2,
+ status_code: "unavailable",
+ extracted_text: "Overflow box",
+ caption: "Overflow box",
+ tags: [],
+ },
+ ],
+ },
+ ]}
+ />,
+ );
+
+ expect(screen.queryByRole("group", { name: "Image regions" })).not.toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: /Image region/ })).not.toBeInTheDocument();
+ expect(screen.getByText("NaN box")).toBeInTheDocument();
+ expect(screen.getByText("Negative box")).toBeInTheDocument();
+ expect(screen.getByText("Zero box")).toBeInTheDocument();
+ expect(screen.getByText("Overflow box")).toBeInTheDocument();
+ expect(screen.getByAltText("Embedded image")).toBeInTheDocument();
+ });
});
From e6fd907ed36d1228729c0df8d6cd22f603051891 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Thu, 20 Aug 2026 18:44:14 +0900
Subject: [PATCH 04/10] docs: allocate unique image overlay ADR number
---
AGENTS.md | 2 +-
...e-region-overlays.md => 0113-buyer-image-region-overlays.md} | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
rename docs/adr/{0112-buyer-image-region-overlays.md => 0113-buyer-image-region-overlays.md} (95%)
diff --git a/AGENTS.md b/AGENTS.md
index 7431440f5..d7743a371 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -130,7 +130,7 @@ contextual-orchestrator owns model discovery and selection.
`This post is an image` to a buyer. When a source image can be reattached,
draw only persisted visual-region boxes; never invent coordinates. If the
bitmap is missing or a box is invalid, keep the region list and do not
- fabricate a locator (ADR 0110 / 0112).
+ fabricate a locator (ADR 0110 / 0113).
## Source parsing and semantic units
diff --git a/docs/adr/0112-buyer-image-region-overlays.md b/docs/adr/0113-buyer-image-region-overlays.md
similarity index 95%
rename from docs/adr/0112-buyer-image-region-overlays.md
rename to docs/adr/0113-buyer-image-region-overlays.md
index b101aa0d1..57c7b51d0 100644
--- a/docs/adr/0112-buyer-image-region-overlays.md
+++ b/docs/adr/0113-buyer-image-region-overlays.md
@@ -1,4 +1,4 @@
-# ADR 0112: Overlay persisted visual-region boxes on the source image
+# ADR 0113: Overlay persisted visual-region boxes on the source image
- Status: Accepted
- Date: 2026-08-20
From 2da6a18ff0c03c95a90bc2f08950522ff1860f8a Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Fri, 21 Aug 2026 07:55:24 +0900
Subject: [PATCH 05/10] fix: validate embedded image sources at render boundary
---
CHANGELOG.d/2.13.2-image-region-overlays.md | 2 ++
docs/adr/0110-buyer-image-evidence-rendering.md | 3 +++
frontend/src/PostBody.test.tsx | 13 +++++++++++++
frontend/src/PostBody.tsx | 13 +++++++++----
4 files changed, 27 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.d/2.13.2-image-region-overlays.md b/CHANGELOG.d/2.13.2-image-region-overlays.md
index a8aa057da..3a5f09c7b 100644
--- a/CHANGELOG.d/2.13.2-image-region-overlays.md
+++ b/CHANGELOG.d/2.13.2-image-region-overlays.md
@@ -13,3 +13,5 @@
- Image-region overlay and current-region labels are translated through the
five-locale catalog (Korean, English fallback, Chinese, Japanese,
Vietnamese).
+- Buyer image rendering now rejects script, SVG, external, and malformed
+ source URLs before they reach an image element.
diff --git a/docs/adr/0110-buyer-image-evidence-rendering.md b/docs/adr/0110-buyer-image-evidence-rendering.md
index 803d75939..40b9590dd 100644
--- a/docs/adr/0110-buyer-image-evidence-rendering.md
+++ b/docs/adr/0110-buyer-image-evidence-rendering.md
@@ -16,6 +16,9 @@ placeholder and is not buyer content.
- Render a source image with its accessible caption, OCR, tags, and region
evidence when the raw data URI is present.
+- At the render boundary, accept only canonical base64 data URIs for inert
+ raster image media types. Reject external, script, SVG, and malformed
+ sources and fall back to persisted caption/OCR/region evidence.
- When the source image cannot be reattached, render only the persisted
`PostImageContent` evidence in a figure; never render the image unit's
internal `unit_text` as buyer prose.
diff --git a/frontend/src/PostBody.test.tsx b/frontend/src/PostBody.test.tsx
index 06071e558..abc66e1ab 100644
--- a/frontend/src/PostBody.test.tsx
+++ b/frontend/src/PostBody.test.tsx
@@ -46,6 +46,19 @@ describe("PostBody", () => {
expect(screen.queryByAltText(/character offset/i)).not.toBeInTheDocument();
});
+ it("rejects script and active SVG image sources before browser rendering", () => {
+ const { rerender } = render('} />);
+
+ expect(screen.queryByRole("img")).not.toBeInTheDocument();
+
+ rerender(
+ '} />,
+ );
+
+ expect(screen.queryByRole("img")).not.toBeInTheDocument();
+ expect(screen.getByText("Embedded image")).toBeInTheDocument();
+ });
+
it("renders authoritative LLM structure levels for semantic list units", () => {
render(
Number.isFinite(value))) {
@@ -27,14 +30,16 @@ function ImageEvidenceFigure({
}) {
const [selectedRegionIndex, setSelectedRegionIndex] = useState(null);
const regions = imageContent?.regions ?? [];
- const overlayRegions = sourceImage ? regions.filter(hasPersistedOverlayBox) : [];
+ const sourceImageSrc =
+ sourceImage && SAFE_EMBEDDED_IMAGE_SOURCE.test(sourceImage.src) ? sourceImage.src : undefined;
+ const overlayRegions = sourceImageSrc ? regions.filter(hasPersistedOverlayBox) : [];
const selectedRegion = overlayRegions.find((region) => region.region_index === selectedRegionIndex);
return (
- {sourceImage ? (
+ {sourceImageSrc ? (
-

+

{overlayRegions.length ? (
{overlayRegions.map((region) => {
@@ -66,7 +71,7 @@ function ImageEvidenceFigure({
) : null}
) : null}
- {imageContent?.caption || !sourceImage ? (
+ {imageContent?.caption || !sourceImageSrc ? (
{imageContent?.caption || t("Embedded image")}
) : null}
{selectedRegion ? (
From a550f8fab304fbff4b1a94b8a44121036aef2067 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Fri, 21 Aug 2026 15:10:45 +0900
Subject: [PATCH 06/10] fix: preserve buyer image region evidence
---
frontend/src/PostBody.tsx | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/frontend/src/PostBody.tsx b/frontend/src/PostBody.tsx
index 13a0f2d58..4113eaec1 100644
--- a/frontend/src/PostBody.tsx
+++ b/frontend/src/PostBody.tsx
@@ -142,13 +142,15 @@ function ImageEvidenceFigure({
{regions.map((region) => (
{region.caption ? {region.caption}
: null}
- {region.extracted_text ? (
+ {region.extracted_text && parsePipeDelimitedTable(region.extracted_text) ? (
{renderImageText(region.extracted_text)}
- ) : region.caption ? null : (
+ ) : !region.caption && region.extracted_text ? (
+ {region.extracted_text}
+ ) : !region.caption ? (
t("Unknown")
- )}
+ ) : null}
{region.tags.length ? (
{t("Image tags")}: {region.tags.join(", ")}
From 5336b288e9b2eadc899024f6de6d45fd0dfc8746 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Fri, 21 Aug 2026 15:11:44 +0900
Subject: [PATCH 07/10] fix: preserve image region evidence during restack
---
frontend/src/PostBody.tsx | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/frontend/src/PostBody.tsx b/frontend/src/PostBody.tsx
index 7ed322219..713e605c2 100644
--- a/frontend/src/PostBody.tsx
+++ b/frontend/src/PostBody.tsx
@@ -141,12 +141,13 @@ function ImageEvidenceFigure({
{regions.map((region) => (
-
- {region.caption ?
{region.caption}
: null}
- {region.extracted_text ? (
+ {region.caption ? (
+ {region.caption}
+ ) : region.extracted_text ? (
{renderImageText(region.extracted_text)}
- ) : region.caption ? null : (
+ ) : (
{t("Unknown")}
)}
{region.tags.length ? (
From 8b37682e3fb906c71a2456a446591dacbe63e5bc Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Fri, 21 Aug 2026 16:21:52 +0900
Subject: [PATCH 08/10] fix: preserve buyer chrome across image overlays
---
CHANGELOG.d/2.13.2-image-region-overlays.md | 3 +
docs/adr/0113-buyer-image-region-overlays.md | 8 +-
frontend/src/App.css | 174 ++++++++++++++++++-
frontend/src/PostBody.test.tsx | 67 +++++++
frontend/src/PostBody.tsx | 2 +-
frontend/src/styles/tokens.test.ts | 16 ++
6 files changed, 262 insertions(+), 8 deletions(-)
diff --git a/CHANGELOG.d/2.13.2-image-region-overlays.md b/CHANGELOG.d/2.13.2-image-region-overlays.md
index 3a5f09c7b..7ae754a87 100644
--- a/CHANGELOG.d/2.13.2-image-region-overlays.md
+++ b/CHANGELOG.d/2.13.2-image-region-overlays.md
@@ -15,3 +15,6 @@
Vietnamese).
- Buyer image rendering now rejects script, SVG, external, and malformed
source URLs before they reach an image element.
+- The login, application shell, footer, and shared button styles remain
+ defined while the overlay stylesheet evolves; a new post image also clears
+ any selection from the previous post.
diff --git a/docs/adr/0113-buyer-image-region-overlays.md b/docs/adr/0113-buyer-image-region-overlays.md
index 57c7b51d0..2742949ac 100644
--- a/docs/adr/0113-buyer-image-region-overlays.md
+++ b/docs/adr/0113-buyer-image-region-overlays.md
@@ -24,9 +24,15 @@ see which panel of the source image supplied that evidence.
only from persisted region caption or extracted text.
- Translate overlay and current-region labels through the five-locale UI
catalog.
+- Reset the selected region when the rendered source image changes, so a
+ stayed-open post popup cannot carry a selection into another post.
+- Keep the shared login, header, footer, and button selectors defined in the
+ same stylesheet; image-overlay work must not remove application chrome.
## Consequences
Buyers can locate panel-level evidence on the source image. Search and
embedding remain bound to the existing region tables. A missing bitmap still
-fails closed to the list rather than fabricating a locator.
+fails closed to the list rather than fabricating a locator, and moving between
+posts does not show a stale selection. The shared-chrome selector regression
+also keeps the login and application shell usable while this feature evolves.
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 9f905f0e6..6b3e6183e 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -1,8 +1,159 @@
-#root {
- max-width: 960px;
- margin: 0 auto;
+/* App shell, login, and shared controls. Keep these selectors aligned with
+ App.tsx so a visual feature cannot silently remove the product chrome. */
+.app-shell {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ width: 100%;
+}
+
+.app-shell > main {
+ flex: 1;
padding: 1.5rem;
- text-align: left;
+}
+
+.login-screen {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 4rem 1.5rem;
+ background: var(--bg);
+}
+
+.login-card {
+ width: 100%;
+ max-width: 420px;
+ padding: 2.5rem 2rem;
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ background: var(--surface);
+ box-shadow: var(--shadow);
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ text-align: center;
+}
+
+.login-header h1 {
+ font-size: 2rem;
+ color: var(--color-primary);
+ margin: 0 0 0.4rem;
+}
+
+.login-subtitle {
+ font-size: 0.9rem;
+ color: var(--text-muted);
+ margin: 0;
+}
+
+.login-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.login-controls button {
+ width: 100%;
+ min-height: 2.75rem;
+}
+
+.login-help {
+ color: var(--text-muted);
+ font-size: 0.8rem;
+}
+
+.app-header-logo,
+.app-header-top-menu {
+ display: flex;
+ align-items: center;
+}
+
+.app-header-logo {
+ gap: 0.75rem;
+}
+
+.app-header-title {
+ font-size: 1.4rem;
+ font-weight: 700;
+ margin: 0;
+ color: var(--color-primary);
+}
+
+.app-header-top-menu {
+ gap: 0.85rem;
+}
+
+.app-user-profile {
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: var(--color-text-heading);
+ background: var(--color-table-row-hover);
+ padding: 0.25rem 0.75rem;
+ border-radius: 999px;
+ border: 1px solid var(--border);
+}
+
+.mobile-drawer-trigger {
+ display: none;
+ background: transparent;
+ border: none;
+ font-size: 1.5rem;
+ cursor: pointer;
+ color: var(--color-text-heading);
+}
+
+.app-footer {
+ margin-top: auto;
+ padding: 1.25rem 1.5rem;
+ min-height: var(--footer-min-height);
+ background: var(--color-footer-bg);
+ border-top: 1px solid var(--color-footer-border);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 0.82rem;
+ color: var(--color-footer-text);
+}
+
+.app-footer-title {
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ color: var(--color-primary);
+}
+
+.app-footer-copyright {
+ color: var(--color-footer-text);
+}
+
+.btn-primary,
+.btn-secondary {
+ border-radius: var(--radius-control);
+ padding: 0.5rem 1.15rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background-color 0.15s ease-in-out;
+}
+
+.btn-primary {
+ background: var(--color-btn-primary-bg);
+ color: var(--color-btn-primary-text);
+ border: 1px solid transparent;
+}
+
+.btn-primary:hover {
+ background: var(--color-btn-primary-hover);
+}
+
+.btn-secondary {
+ background: var(--color-btn-secondary-bg);
+ color: var(--color-btn-secondary-text);
+ border: 1px solid var(--color-btn-secondary-border);
+ padding: 0.45rem 1rem;
+}
+
+.btn-secondary:hover {
+ background: var(--color-btn-secondary-hover);
}
.centered {
@@ -11,10 +162,18 @@
}
.app-header {
+ position: sticky;
+ top: 0;
+ z-index: var(--z-header);
+ background: var(--color-header-bg);
+ border-bottom: 1px solid var(--color-header-border);
display: flex;
justify-content: space-between;
align-items: center;
+ padding: 0 1.5rem;
+ min-height: var(--header-height);
margin-bottom: 1.5rem;
+ box-shadow: 0 1px 4px rgb(0 0 0 / 4%);
}
.app-header > div {
@@ -241,18 +400,21 @@
display: flex;
align-items: center;
justify-content: center;
+ z-index: var(--z-modal-backdrop);
}
.popup-panel {
position: relative;
- background: canvas;
- color: canvastext;
+ background: var(--surface);
+ color: var(--text);
max-width: 720px;
width: 90%;
max-height: 85vh;
overflow-y: auto;
padding: 2rem;
border-radius: 12px;
+ z-index: var(--z-modal);
+ box-shadow: var(--shadow);
}
.popup-close {
diff --git a/frontend/src/PostBody.test.tsx b/frontend/src/PostBody.test.tsx
index d786607fe..8016e44b1 100644
--- a/frontend/src/PostBody.test.tsx
+++ b/frontend/src/PostBody.test.tsx
@@ -495,4 +495,71 @@ describe("PostBody", () => {
expect(screen.getByText("Before").compareDocumentPosition(screen.getByAltText("Source diagram")) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(screen.getByAltText("Source diagram").compareDocumentPosition(screen.getByText("After")) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});
+
+ it("clears a selected region when the displayed post image changes", async () => {
+ const user = userEvent.setup();
+ const firstSource =
+ '
';
+ const secondSource = '
';
+ const structureUnits = [
+ {
+ unit_index: 0,
+ unit_kind_code: "image" as const,
+ unit_label: "img",
+ unit_text: "This post is an image. Ask questions to read its text.",
+ indent_level: 0,
+ indent_source_code: "unresolved" as const,
+ indent_confidence: 0,
+ indent_evidence: "",
+ },
+ ];
+ const region = {
+ region_index: 0,
+ x_ratio: 0.1,
+ y_ratio: 0.2,
+ width_ratio: 0.3,
+ height_ratio: 0.4,
+ status_code: "described",
+ extracted_text: "Region OCR",
+ caption: "Main panel",
+ tags: [],
+ };
+ const { rerender } = render(
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: /Main panel/ }));
+ expect(screen.getByText(/Current image region: Main panel/)).toBeInTheDocument();
+
+ rerender(
+ ,
+ );
+
+ expect(screen.getByAltText("Second diagram")).toBeInTheDocument();
+ expect(screen.queryByText(/Current image region/)).not.toBeInTheDocument();
+ });
});
diff --git a/frontend/src/PostBody.tsx b/frontend/src/PostBody.tsx
index 4113eaec1..f15d82265 100644
--- a/frontend/src/PostBody.tsx
+++ b/frontend/src/PostBody.tsx
@@ -186,7 +186,7 @@ function renderSegment(segment: PostBodySegment, index: number, imageContent?: P
case "image":
return (
diff --git a/frontend/src/styles/tokens.test.ts b/frontend/src/styles/tokens.test.ts
index 54432b43a..6b453f36c 100644
--- a/frontend/src/styles/tokens.test.ts
+++ b/frontend/src/styles/tokens.test.ts
@@ -47,6 +47,16 @@ const RETIRED_LIGHT_ONLY_HEX = [
"#721c24",
];
+const APP_CHROME_SELECTORS = [
+ ".app-shell",
+ ".login-screen",
+ ".login-card",
+ ".app-header-title",
+ ".app-footer",
+ ".btn-primary",
+ ".btn-secondary",
+];
+
describe("design tokens", () => {
it("defines every badge/accent token in both the light and dark blocks", () => {
for (const token of BADGE_AND_ACCENT_TOKENS) {
@@ -70,4 +80,10 @@ describe("design tokens", () => {
expect(appCss, `App.css never references var(${token})`).toContain(`var(${token})`);
}
});
+
+ it("keeps the login and application chrome selectors defined", () => {
+ for (const selector of APP_CHROME_SELECTORS) {
+ expect(appCss, `${selector} must keep a buyer-facing style`).toContain(selector);
+ }
+ });
});
From 519a8c3a9d75ea3def1d6d4febaa6606a7166cbd Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Fri, 21 Aug 2026 17:19:51 +0900
Subject: [PATCH 09/10] fix: keep evidence panel above buyer header
---
frontend/src/App.css | 2 +-
frontend/src/styles/tokens.test.ts | 7 +++++++
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 6b3e6183e..90932fc7a 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -1350,7 +1350,7 @@
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.25);
padding: 2rem 1.5rem;
overflow-y: auto;
- z-index: 10;
+ z-index: var(--z-evidence-panel);
animation: slide-in-from-right 0.2s ease-out;
}
diff --git a/frontend/src/styles/tokens.test.ts b/frontend/src/styles/tokens.test.ts
index 6b453f36c..91cbebfc0 100644
--- a/frontend/src/styles/tokens.test.ts
+++ b/frontend/src/styles/tokens.test.ts
@@ -86,4 +86,11 @@ describe("design tokens", () => {
expect(appCss, `${selector} must keep a buyer-facing style`).toContain(selector);
}
});
+
+ it("keeps the evidence panel above the sticky header layer", () => {
+ expect(lightBlock).toContain("--z-header: 100;");
+ expect(lightBlock).toContain("--z-evidence-panel: 250;");
+ expect(appCss).toContain(".evidence-panel");
+ expect(appCss).toContain("z-index: var(--z-evidence-panel);");
+ });
});
From 57e75d5f8a00132849d655e1ebc7ddf56d73cff3 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Fri, 21 Aug 2026 18:06:01 +0900
Subject: [PATCH 10/10] fix: preserve image region evidence state
---
frontend/src/App.css | 8 ++++-
frontend/src/PostBody.test.tsx | 58 +++++++++++++++++++++++++++---
frontend/src/PostBody.tsx | 18 +++++-----
frontend/src/styles/tokens.test.ts | 7 ++++
4 files changed, 76 insertions(+), 15 deletions(-)
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 90932fc7a..99cc50020 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -199,7 +199,7 @@
}
.error {
- color: #b91c1c;
+ color: var(--color-status-alert);
}
.post-list {
@@ -526,6 +526,7 @@
.post-image-region-overlays {
position: absolute;
inset: 0;
+ pointer-events: none;
}
.post-image-region-overlay {
@@ -537,6 +538,7 @@
background: var(--color-accent-info-background);
cursor: pointer;
appearance: none;
+ pointer-events: auto;
}
.post-image-region-overlay[aria-pressed="true"] {
@@ -555,6 +557,10 @@
color: var(--text-h);
}
+.post-image-current-region:empty {
+ display: none;
+}
+
@media (prefers-reduced-motion: no-preference) {
.post-image-region-overlay {
transition: border-color 160ms ease, background-color 160ms ease;
diff --git a/frontend/src/PostBody.test.tsx b/frontend/src/PostBody.test.tsx
index 2688ac7f7..23f29f85b 100644
--- a/frontend/src/PostBody.test.tsx
+++ b/frontend/src/PostBody.test.tsx
@@ -377,7 +377,7 @@ describe("PostBody", () => {
expect(screen.getByRole("button", { name: "Image region: Edge panel" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /Overflow panel/ })).not.toBeInTheDocument();
expect(screen.queryByText(/This post is an image/)).not.toBeInTheDocument();
- expect(screen.queryByText(/Invented box must not render/)).not.toBeInTheDocument();
+ expect(screen.getByText("Invented box must not render")).toBeInTheDocument();
expect(screen.getByText("Overflow panel")).toBeInTheDocument();
overlay.focus();
@@ -467,10 +467,10 @@ describe("PostBody", () => {
expect(screen.queryByRole("group", { name: "Image regions" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /Image region/ })).not.toBeInTheDocument();
- expect(screen.getByText("NaN box")).toBeInTheDocument();
- expect(screen.getByText("Negative box")).toBeInTheDocument();
- expect(screen.getByText("Zero box")).toBeInTheDocument();
- expect(screen.getByText("Overflow box")).toBeInTheDocument();
+ expect(screen.getAllByText("NaN box")).toHaveLength(2);
+ expect(screen.getAllByText("Negative box")).toHaveLength(2);
+ expect(screen.getAllByText("Zero box")).toHaveLength(2);
+ expect(screen.getAllByText("Overflow box")).toHaveLength(2);
expect(screen.getByAltText("Embedded image")).toBeInTheDocument();
});
@@ -624,4 +624,52 @@ describe("PostBody", () => {
expect(screen.getByAltText("Second diagram")).toBeInTheDocument();
expect(screen.queryByText(/Current image region/)).not.toBeInTheDocument();
});
+
+ it("clears a selected region when region evidence changes for the same image", async () => {
+ const user = userEvent.setup();
+ const source =
+ '
';
+ const structureUnits = [
+ {
+ unit_index: 0,
+ unit_kind_code: "image" as const,
+ unit_label: "img",
+ unit_text: "internal image instruction",
+ indent_level: 0,
+ indent_source_code: "unresolved" as const,
+ indent_confidence: 0,
+ indent_evidence: "",
+ },
+ ];
+ const makeImageContent = (caption: string) => [{
+ unit_index: 0,
+ mime_type: "image/png",
+ status_code: "described",
+ extracted_text: "OCR",
+ caption: "Same diagram",
+ tags: [],
+ regions: [{
+ region_index: 0,
+ x_ratio: 0.1,
+ y_ratio: 0.2,
+ width_ratio: 0.3,
+ height_ratio: 0.4,
+ status_code: "described",
+ extracted_text: "Region OCR",
+ caption,
+ tags: [],
+ }],
+ }];
+ const { rerender } = render(
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: /First panel/ }));
+ expect(screen.getByText("Current image region: First panel")).toBeInTheDocument();
+ rerender(
+ ,
+ );
+
+ expect(screen.queryByText(/Current image region/)).not.toBeInTheDocument();
+ });
});
diff --git a/frontend/src/PostBody.tsx b/frontend/src/PostBody.tsx
index 46d13f01d..e8a8ddb57 100644
--- a/frontend/src/PostBody.tsx
+++ b/frontend/src/PostBody.tsx
@@ -1,4 +1,4 @@
-import { useState, type ReactNode } from "react";
+import { useEffect, useState, type ReactNode } from "react";
import { splitPostBody, type PostBodySegment } from "./postBodyDisplay";
import { t } from "./i18n";
import type { PostContentUnit, PostImageContent, PostImageRegion } from "./api";
@@ -82,6 +82,10 @@ function ImageEvidenceFigure({
const overlayRegions = sourceImageSrc ? regions.filter(hasPersistedOverlayBox) : [];
const selectedRegion = overlayRegions.find((region) => region.region_index === selectedRegionIndex);
+ useEffect(() => {
+ setSelectedRegionIndex(null);
+ }, [sourceImageSrc, imageContent?.unit_index, imageContent?.caption, imageContent?.regions]);
+
return (
{sourceImageSrc ? (
@@ -121,11 +125,9 @@ function ImageEvidenceFigure({
{imageContent?.caption || !sourceImageSrc ? (
{imageContent?.caption || t("Embedded image")}
) : null}
- {selectedRegion ? (
-
- {t("Current image region")}: {regionBuyerLabel(selectedRegion)}
-
- ) : null}
+
+ {selectedRegion ? `${t("Current image region")}: ${regionBuyerLabel(selectedRegion)}` : null}
+
{imageContent?.tags.length ? (
{t("Image tags")}: {imageContent.tags.join(", ")}
@@ -144,12 +146,10 @@ function ImageEvidenceFigure({
{regions.map((region) => (
-
{region.caption ?
{region.caption}
: null}
- {region.extracted_text && parsePipeDelimitedTable(region.extracted_text) ? (
+ {region.extracted_text ? (
{renderImageText(region.extracted_text)}
- ) : !region.caption && region.extracted_text ? (
- {region.extracted_text}
) : !region.caption ? (
t("Unknown")
) : null}
diff --git a/frontend/src/styles/tokens.test.ts b/frontend/src/styles/tokens.test.ts
index 91cbebfc0..d9e8bd1e9 100644
--- a/frontend/src/styles/tokens.test.ts
+++ b/frontend/src/styles/tokens.test.ts
@@ -93,4 +93,11 @@ describe("design tokens", () => {
expect(appCss).toContain(".evidence-panel");
expect(appCss).toContain("z-index: var(--z-evidence-panel);");
});
+
+ it("keeps image overlays transparent outside their accessible controls", () => {
+ expect(appCss).toContain(".post-image-region-overlays");
+ expect(appCss).toContain("pointer-events: none;");
+ expect(appCss).toContain(".post-image-region-overlay");
+ expect(appCss).toContain("pointer-events: auto;");
+ });
});