import { FolkShape } from "./folk-shape"; import { css, html } from "./tags"; export type GoogleService = "gmail" | "drive" | "photos" | "calendar"; export type ItemVisibility = "local" | "shared"; const SERVICE_ICONS: Record = { gmail: "\u{1F4E7}", drive: "\u{1F4C1}", photos: "\u{1F4F7}", calendar: "\u{1F4C5}", }; const styles = css` :host { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); min-width: 180px; min-height: 60px; } .card { width: 100%; height: 100%; padding: 12px; box-sizing: border-box; position: relative; cursor: move; } .card.local { border: 2px dashed #6366f1; } .card.shared { border: 2px solid #22c55e; } .visibility-badge { position: absolute; top: 8px; right: 8px; font-size: 12px; cursor: pointer; } .visibility-badge:hover { transform: scale(1.2); } .header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } .service-icon { font-size: 20px; } .title { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; font-weight: 600; color: #1e293b; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; } .preview { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 12px; color: #64748b; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } .date { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 11px; color: #94a3b8; margin-top: 8px; } .thumbnail { width: 100%; height: 80px; object-fit: cover; border-radius: 4px; margin-top: 8px; } /* Dark mode support */ @media (prefers-color-scheme: dark) { :host { background: #1e293b; } .title { color: #f1f5f9; } .preview { color: #94a3b8; } } `; declare global { interface HTMLElementTagNameMap { "folk-google-item": FolkGoogleItem; } } export class FolkGoogleItem extends FolkShape { static override tagName = "folk-google-item"; static { const sheet = new CSSStyleSheet(); const parentRules = Array.from(FolkShape.styles.cssRules) .map((r) => r.cssText) .join("\n"); const childRules = Array.from(styles.cssRules) .map((r) => r.cssText) .join("\n"); sheet.replaceSync(`${parentRules}\n${childRules}`); this.styles = sheet; } #itemId = ""; #service: GoogleService = "drive"; #title = "Untitled"; #preview = ""; #date: number = Date.now(); #thumbnailUrl = ""; #visibility: ItemVisibility = "local"; get itemId() { return this.#itemId; } set itemId(value: string) { this.#itemId = value; this.requestUpdate("itemId"); } get service() { return this.#service; } set service(value: GoogleService) { this.#service = value; this.requestUpdate("service"); } get title() { return this.#title; } set title(value: string) { this.#title = value; this.requestUpdate("title"); } get preview() { return this.#preview; } set preview(value: string) { this.#preview = value; this.requestUpdate("preview"); } get date() { return this.#date; } set date(value: number) { this.#date = value; this.requestUpdate("date"); } get thumbnailUrl() { return this.#thumbnailUrl; } set thumbnailUrl(value: string) { this.#thumbnailUrl = value; this.requestUpdate("thumbnailUrl"); } get visibility() { return this.#visibility; } set visibility(value: ItemVisibility) { this.#visibility = value; this.requestUpdate("visibility"); } override createRenderRoot() { const root = super.createRenderRoot(); // Read attributes this.#itemId = this.getAttribute("item-id") || ""; this.#service = (this.getAttribute("service") as GoogleService) || "drive"; this.#title = this.getAttribute("title") || "Untitled"; this.#preview = this.getAttribute("preview") || ""; this.#date = parseInt(this.getAttribute("date") || String(Date.now()), 10); this.#thumbnailUrl = this.getAttribute("thumbnail-url") || ""; this.#visibility = (this.getAttribute("visibility") as ItemVisibility) || "local"; const wrapper = document.createElement("div"); wrapper.innerHTML = html`
${this.#visibility === "local" ? "\u{1F512}" : "\u{1F310}"}
${SERVICE_ICONS[this.#service]} ${this.#escapeHtml(this.#title)}
${this.#preview ? `
${this.#escapeHtml(this.#preview)}
` : ""}
${this.#formatDate(this.#date)}
${ this.#thumbnailUrl && this.height > 100 ? `` : "" }
`; // Replace the container div (slot's parent) with our card const containerDiv = root.querySelector(":scope > div"); const cardEl = wrapper.querySelector(".card"); if (containerDiv && cardEl) { containerDiv.replaceWith(cardEl); } // Toggle visibility on badge click const badge = root.querySelector(".visibility-badge") as HTMLElement; const card = root.querySelector(".card") as HTMLElement; badge?.addEventListener("click", (e) => { e.stopPropagation(); this.#visibility = this.#visibility === "local" ? "shared" : "local"; badge.textContent = this.#visibility === "local" ? "\u{1F512}" : "\u{1F310}"; card.classList.remove("local", "shared"); card.classList.add(this.#visibility); this.dispatchEvent( new CustomEvent("visibility-change", { detail: { visibility: this.#visibility, itemId: this.#itemId }, bubbles: true, }) ); }); return root; } #escapeHtml(text: string): string { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } #formatDate(timestamp: number): string { const now = new Date(); const date = new Date(timestamp); const diffDays = Math.floor( (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24) ); if (diffDays === 0) return "Today"; if (diffDays === 1) return "Yesterday"; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); } override toJSON() { return { ...super.toJSON(), type: "folk-google-item", itemId: this.itemId, service: this.service, title: this.title, preview: this.preview, date: this.date, thumbnailUrl: this.thumbnailUrl, visibility: this.visibility, }; } } /** * Helper to create Google item props */ export function createGoogleItemProps( service: GoogleService, title: string, options: Partial<{ itemId: string; preview: string; date: number; thumbnailUrl: string; visibility: ItemVisibility; }> = {} ) { return { service, title, itemId: options.itemId || crypto.randomUUID(), preview: options.preview || "", date: options.date || Date.now(), thumbnailUrl: options.thumbnailUrl || "", visibility: options.visibility || "local", }; }