fix(preview): refine source citation highlights (#4139)

* fix(preview): refine source citation highlights

* fix(preview): use shared radius token for source highlights
This commit is contained in:
lyingbug
2026-10-10 20:03:10 +08:00
committed by GitHub
parent aa535c22fe
commit 50a2963160
3 changed files with 60 additions and 24 deletions
+9 -9
View File
@@ -217,18 +217,18 @@ select:-webkit-autofill {
--app-accent-purple: #9d7cff;
}
/* 引用原文定位:低饱和薄荷绿,文字轻染色,较大区域用浅底和细描边。 */
/* 引用原文定位:纸面荧光笔色,文字与整块区域使用不同强度。 */
:root {
--app-source-highlight: #3b9b83;
--app-source-highlight-bg: color-mix(in srgb, var(--app-source-highlight) 20%, transparent);
--app-source-highlight-soft-bg: color-mix(in srgb, var(--app-source-highlight) 8%, transparent);
--app-source-highlight-border: color-mix(in srgb, var(--app-source-highlight) 40%, transparent);
--app-source-highlight: #d6a124;
--app-source-highlight-bg: rgba(250, 204, 75, 0.34);
--app-source-highlight-soft-bg: rgba(250, 204, 75, 0.09);
--app-source-highlight-border: rgba(183, 130, 22, 0.5);
}
:root[theme-mode="dark"] {
--app-source-highlight: #71c4aa;
--app-source-highlight-bg: color-mix(in srgb, var(--app-source-highlight) 24%, transparent);
--app-source-highlight-soft-bg: color-mix(in srgb, var(--app-source-highlight) 10%, transparent);
--app-source-highlight-border: color-mix(in srgb, var(--app-source-highlight) 48%, transparent);
--app-source-highlight: #e6b94f;
--app-source-highlight-bg: rgba(230, 185, 79, 0.26);
--app-source-highlight-soft-bg: rgba(230, 185, 79, 0.08);
--app-source-highlight-border: rgba(230, 185, 79, 0.5);
}
/* 全局旋转动画:spinner / 刷新图标统一用 `animation: wk-spin 0.8s linear infinite` */
+19 -3
View File
@@ -1686,16 +1686,32 @@ mark.source-locate-mark {
background-color: var(--app-source-highlight-bg);
color: inherit;
border-radius: var(--app-radius-xs);
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}
.source-locate-block {
background-color: var(--app-source-highlight-soft-bg) !important;
outline: 1px solid var(--app-source-highlight-border);
outline-offset: 2px;
border-radius: var(--app-radius-xs);
box-shadow: inset 3px 0 var(--app-source-highlight-border);
}
/* Cell backgrounds can obscure a row's marker. Paint each cell once and
keep the accent on the leading edge, without boxing every table row. */
tr.source-locate-block {
background-color: transparent !important;
box-shadow: none;
}
tr.source-locate-block > td {
background-color: var(--app-source-highlight-soft-bg) !important;
}
tr.source-locate-block > td:first-child {
box-shadow: inset 3px 0 var(--app-source-highlight-border);
}
img.source-locate-block {
outline: 1px solid var(--app-source-highlight-border);
outline-offset: 2px;
}
</style>
@@ -10,7 +10,7 @@ type PdfDocument = Awaited<ReturnType<PdfJs['getDocument']>['promise']>
type PdfPage = Awaited<ReturnType<PdfDocument['getPage']>>
/** A highlight drawn on a page, in fractions of the page box. */
type Mark = { left: number; top: number; width: number; height: number; kind: 'box' | 'text' | 'page' }
type Mark = { left: number; top: number; width: number; height: number; kind: 'box' | 'text' | 'page'; target: number }
export type PdfLocateResult = { found: boolean; precise: boolean; page?: number; granularity?: string; reason?: string }
@@ -277,7 +277,7 @@ async function pageText(pageNumber: number): Promise<string> {
}
/** Refine only within the recorded region, or at a verified text occurrence. */
async function markTarget(target: PdfTarget, version: number): Promise<number | null> {
async function markTarget(target: PdfTarget, version: number, targetIndex: number): Promise<number | null> {
const pageNumber = target.page
await renderPage(pageNumber)
await rendered.get(pageNumber)?.done
@@ -305,7 +305,7 @@ async function markTarget(target: PdfTarget, version: number): Promise<number |
if (!lines?.length) return null
let top = 1
for (const r of lines) {
const mark: Mark = { kind: 'text', left: (r.left - pageBox.left) / pageBox.width,
const mark: Mark = { kind: 'text', target: targetIndex, left: (r.left - pageBox.left) / pageBox.width,
top: (r.top - pageBox.top) / pageBox.height, width: r.width / pageBox.width, height: r.height / pageBox.height }
addMark(pageNumber, mark)
top = Math.min(top, mark.top)
@@ -317,6 +317,7 @@ async function applyLocate(request: SourceLocateRequest | null | undefined) {
const version = ++locateVersion
clearMarks()
locatedTargets.value = []
selectedTarget.value = 0
if (!request || !pdf.value) return
try {
const result = await resolvePdfSource(request, pageCount.value, pageText, () => version !== locateVersion)
@@ -324,18 +325,20 @@ async function applyLocate(request: SourceLocateRequest | null | undefined) {
const destinations: Array<{ page: number; top: number }> = []
const levels: string[] = []
for (const target of result.targets) {
let top = await markTarget(target, version)
// Use the navigation index: unresolved targets do not get a stop.
const targetIndex = destinations.length
let top = await markTarget(target, version, targetIndex)
if (version !== locateVersion) return
if (top !== null) levels.push('text')
else if (target.bbox) {
const [x0, y0, x1, y1] = target.bbox
addMark(target.page, { kind: 'box', left: x0, top: y0, width: x1 - x0, height: y1 - y0 })
addMark(target.page, { kind: 'box', target: targetIndex, left: x0, top: y0, width: x1 - x0, height: y1 - y0 })
top = y0
levels.push('block')
} else if (target.granularity === 'page') {
top = 0
levels.push('page')
addMark(target.page, { kind: 'page', left: 0, top: 0, width: 1, height: 1 })
addMark(target.page, { kind: 'page', target: targetIndex, left: 0, top: 0, width: 1, height: 1 })
}
if (top !== null) destinations.push({ page: target.page, top })
}
@@ -445,7 +448,7 @@ defineExpose({ relocate: () => applyLocate(props.locate) })
v-for="(mark, i) in marks[page] || []"
:key="i"
class="pdf-source-mark"
:class="`pdf-source-mark--${mark.kind}`"
:class="[`pdf-source-mark--${mark.kind}`, { 'is-active': mark.target === selectedTarget }]"
:style="{
left: `${mark.left * 100}%`,
top: `${mark.top * 100}%`,
@@ -511,6 +514,11 @@ defineExpose({ relocate: () => applyLocate(props.locate) })
.pdf-source-page {
position: relative;
isolation: isolate;
// The PDF canvas stays white even when the surrounding app is dark.
--app-source-highlight-bg: rgba(250, 204, 75, 0.34);
--app-source-highlight-soft-bg: rgba(250, 204, 75, 0.09);
--app-source-highlight-border: rgba(183, 130, 22, 0.5);
// Block flow with auto margins keeps zoomed pages scrollable to the left.
margin: 0 auto 12px;
max-width: none;
@@ -533,23 +541,35 @@ defineExpose({ relocate: () => applyLocate(props.locate) })
inset: 0;
z-index: 2;
pointer-events: none;
// Blend the whole layer once, so overlapping glyph rectangles do not
// darken the printed text independently.
mix-blend-mode: multiply;
}
.pdf-source-mark {
position: absolute;
border-radius: var(--app-radius-xs);
background: var(--app-source-highlight-bg);
mix-blend-mode: multiply;
background: var(--app-source-highlight-soft-bg);
&--text.is-active {
background: var(--app-source-highlight-bg);
}
&--box {
background: var(--app-source-highlight-soft-bg);
outline: 1px solid var(--app-source-highlight-border);
&.is-active {
box-shadow: inset 3px 0 var(--app-source-highlight-border);
}
}
&--page {
background: transparent;
outline: 1px solid var(--app-source-highlight-border);
mix-blend-mode: normal;
box-shadow: inset 0 0 0 1px var(--app-source-highlight-border);
&.is-active {
box-shadow: inset 3px 0 var(--app-source-highlight-border), inset 0 0 0 1px var(--app-source-highlight-border);
}
}
}