Add Evidence sidebar text filter for CE-WP-0010.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Filter input next to the Evidence caption; case-insensitive match on quote,
commentary, and status when the query has three or more characters.
This commit is contained in:
tegwick 2026-07-30 19:41:40 +02:00
parent 7f42564cf4
commit 664d7da100

View file

@ -104,6 +104,22 @@ function annotationOrderKey(annotation: Annotation | null): number {
return docOrderKey(annotation.selectors);
}
const FILTER_MIN_CHARS = 3;
function matchesEvidenceFilter(
query: string,
quote: string,
commentary: string | undefined,
status: string,
): boolean {
const q = query.trim();
if (q.length < FILTER_MIN_CHARS) return true;
const needle = q.toLowerCase();
return [quote, commentary ?? "", status].some((p) =>
p.toLowerCase().includes(needle),
);
}
export function EvidenceSidebar(props: EvidenceSidebarProps) {
const engine = useEngine();
const { document } = useActiveDocument();
@ -117,6 +133,8 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
const annotationUpdateTick = useEngineEventTick("AnnotationUpdated");
const revision = useEngineRevision();
const [filterQuery, setFilterQuery] = useState("");
// Build the sorted view-model: each item gets its order key + the
// first annotation up-front so the render below doesn't have to
// re-resolve them inside the map.
@ -139,6 +157,17 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
revision,
]);
const filteredItems = useMemo(() => {
return sortedItems.filter(({ item, annotation }) =>
matchesEvidenceFilter(
filterQuery,
annotation?.quote ?? "",
item.commentary,
item.status,
),
);
}, [sortedItems, filterQuery]);
const pendingOrder = useMemo<number>(() => {
if (!pending) return Number.POSITIVE_INFINITY;
const c = pending.capture;
@ -148,15 +177,15 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
return c.startOffset;
}, [pending]);
// Find the insert position for the pending capture form: first index
// whose order > pendingOrder, or sortedItems.length to append.
// Insert pending capture among the *unfiltered* document order so a
// narrow filter cannot hide the in-progress capture form.
const pendingInsertIndex = useMemo(() => {
if (!pending) return -1;
for (let i = 0; i < sortedItems.length; i++) {
if (sortedItems[i]!.order > pendingOrder) return i;
for (let i = 0; i < filteredItems.length; i++) {
if (filteredItems[i]!.order > pendingOrder) return i;
}
return sortedItems.length;
}, [pending, pendingOrder, sortedItems]);
return filteredItems.length;
}, [pending, pendingOrder, filteredItems]);
const [openExportFor, setOpenExportFor] = useState<EvidenceItemId | null>(null);
const [editingId, setEditingId] = useState<EvidenceItemId | null>(null);
@ -249,6 +278,7 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
return (
<aside
aria-label="Evidence"
style={{
width: 320,
borderLeft: "1px solid #ddd",
@ -259,7 +289,33 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
position: "relative",
}}
>
<h2 style={{ marginTop: 0, fontSize: 16 }}>Evidence</h2>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
marginBottom: 8,
flexWrap: "wrap",
}}
>
<h2 style={{ margin: 0, fontSize: 16, flex: "0 0 auto" }}>Evidence</h2>
<input
type="search"
value={filterQuery}
onChange={(e) => setFilterQuery(e.target.value)}
placeholder="Filter…"
aria-label="Filter evidence"
data-testid="evidence-filter"
style={{
flex: "1 1 100px",
minWidth: 80,
fontSize: 12,
padding: "4px 6px",
border: "1px solid #ccc",
borderRadius: 4,
}}
/>
</div>
{!document && (
<p style={{ fontSize: 12, color: "#888" }}>No document open.</p>
)}
@ -269,8 +325,19 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
capture.
</p>
)}
{document &&
sortedItems.length > 0 &&
filteredItems.length === 0 &&
!pending && (
<p
style={{ fontSize: 12, color: "#888" }}
data-testid="evidence-filter-empty"
>
No matches.
</p>
)}
<ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
{sortedItems.map((entry, i) => {
{filteredItems.map((entry, i) => {
const slotForCapture = pendingInsertIndex === i;
return (
<Fragment key={entry.item.id}>
@ -314,7 +381,7 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
</Fragment>
);
})}
{pendingInsertIndex === sortedItems.length && (
{pendingInsertIndex === filteredItems.length && (
<li>
<InlineCaptureForm />
</li>