feat(search): why this matched highlights (#15)
CI / backend (push) Successful in 50s
CI / frontend (push) Successful in 14s
Deploy Staging / deploy (push) Successful in 29s
Security / secrets (push) Successful in 4s
Security / dependencies (push) Successful in 55s
CI / backend (pull_request) Successful in 47s
CI / frontend (pull_request) Successful in 12s
Security / secrets (pull_request) Successful in 3s
Security / dependencies (pull_request) Successful in 54s

This commit was merged in pull request #15.
This commit is contained in:
2026-07-01 22:22:38 +02:00
parent fcf290a83b
commit 9bbab5d32a
4 changed files with 69 additions and 11 deletions
+22 -1
View File
@@ -16,6 +16,25 @@ const fmtSize = (b) => {
return `${(b / 1048576).toFixed(1)} MB`;
};
// "Why this matched": the API wraps matched terms in U+E000/U+E001 sentinels (NOT HTML).
// We tokenise and render the highlighted parts as <mark> React elements — React escapes
// all text nodes, so untrusted email content can never inject markup (no dangerouslySetInnerHTML).
const HL_START = String.fromCharCode(0xE000);
const HL_STOP = String.fromCharCode(0xE001);
const HL_RE = new RegExp(HL_START + '([\s\S]*?)' + HL_STOP, 'g');
function renderHighlight(s) {
const out = [];
let last = 0, key = 0, m;
HL_RE.lastIndex = 0;
while ((m = HL_RE.exec(s)) !== null) {
if (m.index > last) out.push(s.slice(last, m.index));
out.push(<mark key={key++}>{m[1]}</mark>);
last = HL_RE.lastIndex;
}
if (last < s.length) out.push(s.slice(last));
return out;
}
export default function EmailRow({ email: initial, onRemove, selected, onToggleSelect, focused }) {
const [email, setEmail] = useState(initial);
const [acting, setActing] = useState(false);
@@ -83,7 +102,9 @@ export default function EmailRow({ email: initial, onRemove, selected, onToggleS
</td>
<td className="el-subject">
<span className="el-subj-text">{email.subject || '(no subject)'}</span>
{email.snippet && <span className="el-snippet"> {email.snippet}</span>}
{email.matchHighlight
? <span className="el-snippet"> {renderHighlight(email.matchHighlight)}</span>
: email.snippet && <span className="el-snippet"> {email.snippet}</span>}
</td>
<td className="el-meta">
{email.hasAttachments && <span className="el-attach" title="Has attachment">📎</span>}
+8
View File
@@ -64,6 +64,14 @@
border-color: hsl(var(--border));
}
/* Search "why this matched" highlight — subtle accent tint, not the default yellow. */
mark {
background: hsl(var(--primary) / 0.22);
color: inherit;
border-radius: 3px;
padding: 0 1px;
}
body {
@apply bg-background text-foreground antialiased;
/* Inter (variable, self-hosted via @fontsource-variable/inter); system fallback. */