commit 5735aee7217614ccdbc47a18c0bfd4ded1d31336
parent e1f1b62c70435c18afe811e6489e991b95d306c0
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 2 Jul 2026 11:16:29 -0400
Phase 6b: provenance accent on search result cards
Carry each source archive's accent onto its result cards in hub mode, completing
the provenance throughline (shelf spine → filter group → result row). The data
context gains accentOf(origin): undefined in single-site mode (a stable module
constant, so the hot resultGroups memo doesn't churn), the site's accent in hub
mode. Each ResultGroup records its accent and the card renders it as a left edge.
Single-site is byte-identical (accentOf returns undefined → no style; verified by
share + virtualization e2e); hub e2e green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 42 insertions(+), 3 deletions(-)
diff --git a/common/components/SearchDataContext.tsx b/common/components/SearchDataContext.tsx
@@ -7,7 +7,13 @@
// hands the component one unified, origin-qualified view — so TranscriptSearch
// itself stays mode-agnostic and reads everything from this context.
-import { createContext, useContext, useMemo, type ReactNode } from "react";
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+ type ReactNode,
+} from "react";
import { useQueries } from "@tanstack/react-query";
import { useSummaries, type SummariesState } from "./summariesCache";
import { useSubsManifest } from "./subsCache";
@@ -47,8 +53,16 @@ export type SearchDataValue = {
defaultGroupId: string;
// Map a summary to its channel selection key (matches ChannelOption.key).
channelKeyOf: (t: ChannelKeyable) => string;
+ // Provenance accent for a content origin ("" = same-origin). Returns a color
+ // only in hub mode (each federated site's accent); undefined in single-site
+ // mode, so no provenance marker renders. Stable reference in single-site mode.
+ accentOf: (origin: string) => string | undefined;
};
+// Single-site mode has no provenance accents; a module constant keeps the
+// reference stable so downstream memos (e.g. result groups) don't churn.
+const NO_ACCENT = (): undefined => undefined;
+
const SearchDataContext = createContext<SearchDataValue | null>(null);
export function useSearchData(): SearchDataValue {
@@ -109,6 +123,7 @@ export function SingleSiteDataProvider({ children }: { children: ReactNode }) {
groups,
defaultGroupId,
channelKeyOf: (t) => t.channel,
+ accentOf: NO_ACCENT,
}),
[summariesState, subsManifest, channels, groups, defaultGroupId],
);
@@ -331,6 +346,17 @@ export function MultiSiteDataProvider({
[mergedManifest, summaries, loadedPages, pageDescriptors, summariesReady],
);
+ // origin → accent lookup for provenance in results.
+ const accentByOrigin = useMemo(() => {
+ const m = new Map<string, string>();
+ for (const s of sites) if (s.accent) m.set(s.origin, s.accent);
+ return m;
+ }, [sites]);
+ const accentOf = useCallback(
+ (origin: string) => accentByOrigin.get(origin),
+ [accentByOrigin],
+ );
+
const value = useMemo<SearchDataValue>(
() => ({
summariesState,
@@ -341,8 +367,9 @@ export function MultiSiteDataProvider({
// channelSlug was rewritten to the origin-qualified key above, so it IS
// the selection key (matches ChannelOption.key).
channelKeyOf: (t) => t.channelSlug,
+ accentOf,
}),
- [summariesState, subsManifest, channels, groups, defaultGroupId],
+ [summariesState, subsManifest, channels, groups, defaultGroupId, accentOf],
);
return (
diff --git a/common/components/TranscriptSearch.tsx b/common/components/TranscriptSearch.tsx
@@ -75,6 +75,9 @@ type ResultGroup = {
isLivestream: boolean;
ageRestricted: boolean;
hits: LayerHit[];
+ // Provenance accent of the source origin (hub mode only); undefined
+ // single-site, so no marker renders.
+ accent?: string;
};
const DEFAULT_MAX_HITS = 500;
@@ -262,6 +265,7 @@ export default function TranscriptSearch() {
channels: channelModel,
groups: manifestGroups,
channelKeyOf,
+ accentOf,
} = useSearchData();
const { summaries, loadedPages, pageCount, summariesReady } =
summariesState;
@@ -561,6 +565,7 @@ export default function TranscriptSearch() {
isLivestream: t.isLivestream,
ageRestricted: t.ageRestricted,
hits: [],
+ accent: accentOf(splitId(t.slug).origin),
});
}
return out;
@@ -579,10 +584,11 @@ export default function TranscriptSearch() {
isLivestream: t.isLivestream,
ageRestricted: t.ageRestricted,
hits: hitsBySlug.get(t.slug) ?? [],
+ accent: accentOf(splitId(t.slug).origin),
});
}
return out;
- }, [hasActiveQuery, transcripts, treeProgress, passesFilter]);
+ }, [hasActiveQuery, transcripts, treeProgress, passesFilter, accentOf]);
const leafStates = treeProgress?.leafStates ?? new Map<string, LeafState>();
const groupStates =
@@ -1982,6 +1988,12 @@ function ResultCard({
data-result-slug={group.slug}
data-card-header=""
className="border border-border rounded-lg overflow-hidden bg-card"
+ // Provenance: a left edge in the source archive's accent (hub mode only).
+ style={
+ group.accent
+ ? { borderLeftColor: group.accent, borderLeftWidth: "3px" }
+ : undefined
+ }
>
<Button
type="button"