feat(renderer): add configurable result display fields

Introduce a display fields option so callers can toggle cover, domain, created date, excerpt, tags and collection per result row.

Render an optional cover thumbnail and a collection label resolved from a collection title map.

Defaults keep the previous appearance, so existing views render unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lukas Wolfsteiner 2026-07-06 11:28:15 +02:00
parent a41574e926
commit b643644fb0
2 changed files with 73 additions and 3 deletions

View file

@ -2,10 +2,47 @@ import type { RaindropItem } from "./raindrop-api";
export type RaindropStatusKind = "loading" | "empty" | "error" | "info"; export type RaindropStatusKind = "loading" | "empty" | "error" | "info";
export interface RaindropDisplayFields {
cover: boolean;
domain: boolean;
created: boolean;
excerpt: boolean;
tags: boolean;
collection: boolean;
}
export const DEFAULT_DISPLAY_FIELDS: RaindropDisplayFields = {
cover: false,
domain: true,
created: true,
excerpt: true,
tags: true,
collection: false,
};
export const RAINDROP_FIELD_NAMES = Object.keys(DEFAULT_DISPLAY_FIELDS) as (keyof RaindropDisplayFields)[];
export function isRaindropFieldName(value: string): value is keyof RaindropDisplayFields {
return (RAINDROP_FIELD_NAMES as string[]).includes(value);
}
export function resolveRaindropDisplayFields(
defaults: RaindropDisplayFields,
show?: (keyof RaindropDisplayFields)[],
hide?: (keyof RaindropDisplayFields)[],
): RaindropDisplayFields {
const fields = { ...defaults };
for (const field of show ?? []) fields[field] = true;
for (const field of hide ?? []) fields[field] = false;
return fields;
}
export interface RenderRaindropOptions { export interface RenderRaindropOptions {
title?: string; title?: string;
warnings?: string[]; warnings?: string[];
onTagClick?: (tag: string) => void; onTagClick?: (tag: string) => void;
fields?: RaindropDisplayFields;
collectionTitles?: Map<number, string>;
} }
function getDisplayDate(value: string | undefined): string | undefined { function getDisplayDate(value: string | undefined): string | undefined {
@ -17,6 +54,13 @@ function getDisplayDate(value: string | undefined): string | undefined {
return date.toLocaleDateString(); return date.toLocaleDateString();
} }
function getCollectionTitle(item: RaindropItem, collectionTitles: Map<number, string> | undefined): string | undefined {
const collectionId = item.collection?.$id;
if (typeof collectionId !== "number") return undefined;
return collectionTitles?.get(collectionId);
}
export function renderRaindropStatus(container: HTMLElement, message: string, kind: RaindropStatusKind): void { export function renderRaindropStatus(container: HTMLElement, message: string, kind: RaindropStatusKind): void {
container.empty(); container.empty();
container.createDiv({ container.createDiv({
@ -32,6 +76,7 @@ export function renderRaindropItems(
): void { ): void {
container.empty(); container.empty();
const fields = options.fields ?? DEFAULT_DISPLAY_FIELDS;
const root = container.createDiv({ cls: "raindrop-results" }); const root = container.createDiv({ cls: "raindrop-results" });
if (options.title) { if (options.title) {
root.createEl("h4", { cls: "raindrop-results-title", text: options.title }); root.createEl("h4", { cls: "raindrop-results-title", text: options.title });
@ -52,6 +97,18 @@ export function renderRaindropItems(
const list = root.createDiv({ cls: "raindrop-list" }); const list = root.createDiv({ cls: "raindrop-list" });
for (const item of items) { for (const item of items) {
const row = list.createDiv({ cls: "raindrop-item" }); const row = list.createDiv({ cls: "raindrop-item" });
if (fields.cover && item.cover) {
row.createEl("img", {
cls: "raindrop-item-cover",
attr: {
src: item.cover,
alt: "",
loading: "lazy",
},
});
}
const title = row.createEl("a", { const title = row.createEl("a", {
cls: "raindrop-item-title", cls: "raindrop-item-title",
text: item.title || item.link, text: item.title || item.link,
@ -63,16 +120,20 @@ export function renderRaindropItems(
}); });
title.setAttr("aria-label", `Open ${item.title || item.link}`); title.setAttr("aria-label", `Open ${item.title || item.link}`);
const metaParts = [item.domain, getDisplayDate(item.created)].filter((part): part is string => Boolean(part)); const metaParts = [
fields.collection ? getCollectionTitle(item, options.collectionTitles) : undefined,
fields.domain ? item.domain : undefined,
fields.created ? getDisplayDate(item.created) : undefined,
].filter((part): part is string => Boolean(part));
if (metaParts.length > 0) { if (metaParts.length > 0) {
row.createDiv({ cls: "raindrop-item-meta", text: metaParts.join(" - ") }); row.createDiv({ cls: "raindrop-item-meta", text: metaParts.join(" - ") });
} }
if (item.excerpt) { if (fields.excerpt && item.excerpt) {
row.createDiv({ cls: "raindrop-item-excerpt", text: item.excerpt }); row.createDiv({ cls: "raindrop-item-excerpt", text: item.excerpt });
} }
if (item.tags && item.tags.length > 0) { if (fields.tags && item.tags && item.tags.length > 0) {
const tags = row.createDiv({ cls: "raindrop-item-tags" }); const tags = row.createDiv({ cls: "raindrop-item-tags" });
for (const tag of item.tags) { for (const tag of item.tags) {
const tagEl = tags.createEl("a", { const tagEl = tags.createEl("a", {

View file

@ -22,6 +22,15 @@
background: var(--background-primary); background: var(--background-primary);
} }
.raindrop-item-cover {
border-radius: var(--radius-s);
display: block;
margin-bottom: 0.5rem;
max-height: 8rem;
object-fit: cover;
width: 100%;
}
.raindrop-item-title { .raindrop-item-title {
display: inline-block; display: inline-block;
font-weight: var(--font-semibold); font-weight: var(--font-semibold);