320 lines
7.0 KiB
TypeScript
320 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>
|
|
`;
|
|
|
|
// 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",
|
|
};
|
|
}
|