@@ -328,7 +352,7 @@ export default function UniversalSearchPage() {
)}
-
+
))}
@@ -336,6 +360,72 @@ export default function UniversalSearchPage() {
))}
+
+ {/*
+ Quick overview.
+
+ One generic renderer for all eight result types: the service
+ returns a flat label/value list per hit, so a new source gets a
+ preview without any new UI here.
+
+ `key={previewHit.id}` is deliberate — it remounts the content
+ when the hit changes, so switching results re-runs the
+ initialisers against the new record instead of syncing props
+ into existing state.
+ */}
+
);
}
\ No newline at end of file
diff --git a/src/lib/server/universal-search-service.ts b/src/lib/server/universal-search-service.ts
index 67a37d5..9854cb4 100644
--- a/src/lib/server/universal-search-service.ts
+++ b/src/lib/server/universal-search-service.ts
@@ -93,6 +93,15 @@ const NON_TERMINAL_STATUSES: string[] = Array.from(
),
);
+/**
+ * Field lists for the bounded reference collections. Named so the
+ * search scoring and the quick-overview dialog pull from the SAME cached
+ * read — two different field strings would be two cache keys and two PB
+ * round-trips for the same data.
+ */
+const CUSTOMER_FIELDS = "id,customer,email,number,company";
+const COMPANY_FIELDS = "id,company,address,vendor,company_email,company_tel";
+
/* ============================================================================
* Types
* ========================================================================== */
@@ -117,6 +126,42 @@ export interface UniversalHit {
href: string;
/** Which surface matched, e.g. "SKU" — answers "why is this here?". */
matchedOn?: string;
+ /**
+ * Short label/value rows shown in the quick-overview dialog.
+ *
+ * Deliberately a GENERIC key/value list rather than a typed payload
+ * per entity: one renderer then serves all eight result types, so
+ * adding a ninth source needs no new UI. The per-type shaping lives
+ * here in the service, which already has the data in hand from the
+ * snapshot/reference caches — so opening a preview costs no extra
+ * round-trip.
+ *
+ * Rows are ordered most-identifying-first.
+ */
+ preview?: PreviewRow[];
+}
+
+export interface PreviewRow {
+ label: string;
+ value: string;
+}
+
+/** Drops rows whose value is empty so previews don't render blank lines. */
+function previewRows(rows: Array<[string, unknown]>): PreviewRow[] {
+ return rows
+ .map(([label, value]) => ({
+ label,
+ value: value === null || value === undefined ? "" : String(value).trim(),
+ }))
+ .filter((row) => row.value.length > 0);
+}
+
+/** Truncates long free text for a compact dialog cell. */
+function clip(value: unknown, max = 160): string {
+ const s = value === null || value === undefined ? "" : String(value).trim();
+ if (!s) return "";
+ const oneLine = s.replace(/\s+/g, " ");
+ return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
}
export type SourceStatus = "ok" | "timeout" | "error";
@@ -248,6 +293,8 @@ interface OrderSnapshot {
orders: Record