rspace-online/lib/folk-google-item.ts

322 lines
7.0 KiB
TypeScript

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<GoogleService, string> = {
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`
<div class="card ${this.#visibility}" data-drag>
<span class="visibility-badge" title="Toggle visibility">
${this.#visibility === "local" ? "\u{1F512}" : "\u{1F310}"}
</span>
<div class="header">
<span class="service-icon">${SERVICE_ICONS[this.#service]}</span>
<span class="title">${this.#escapeHtml(this.#title)}</span>
</div>
${this.#preview ? `<div class="preview">${this.#escapeHtml(this.#preview)}</div>` : ""}
<div class="date">${this.#formatDate(this.#date)}</div>
${
this.#thumbnailUrl && this.height > 100
? `<img class="thumbnail" src="${this.#thumbnailUrl}" alt="" />`
: ""
}
</div>
`;
const slot = root.querySelector("slot");
if (slot?.parentElement) {
const parent = slot.parentElement;
const existingDiv = parent.querySelector("div");
if (existingDiv) {
parent.replaceChild(wrapper.querySelector(".card")!, existingDiv);
}
}
// 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",
};
}