import { FolkShape } from "./folk-shape"; import { css, html } from "./tags"; import type { SocialPlatform } from "./folk-social-post"; const PLATFORM_COLORS: Record = { x: "#000000", linkedin: "#0A66C2", instagram: "#E4405F", youtube: "#FF0000", threads: "#000000", bluesky: "#0085FF", tiktok: "#010101", facebook: "#1877F2", }; const styles = css` :host { background: var(--rs-bg-surface, #fff); color: var(--rs-text-primary, #1e293b); border-radius: 12px; box-shadow: var(--rs-shadow-sm); min-width: 260px; min-height: 120px; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } :host(:hover) { box-shadow: var(--rs-shadow-md); } .header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; cursor: move; color: white; border-radius: 12px 12px 0 0; } .header-left { display: flex; align-items: center; gap: 8px; } .header-icon { font-size: 18px; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.2); border-radius: 6px; } .header-title { font-size: 13px; font-weight: 600; letter-spacing: 0.3px; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .header-actions { display: flex; gap: 4px; } .header-actions button { background: rgba(255, 255, 255, 0.2); border: none; color: white; width: 24px; height: 24px; border-radius: 4px; cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; } .header-actions button:hover { background: rgba(255, 255, 255, 0.3); } .body { padding: 12px 14px; } .tweet-count { display: inline-block; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; padding: 2px 8px; border-radius: 10px; background: var(--rs-bg-surface-raised); color: var(--rs-text-muted); margin-bottom: 8px; } .tweet-preview { font-size: 13px; line-height: 1.5; color: var(--rs-text-primary); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 8px; } .cover-image { width: 100%; height: 80px; border-radius: 8px; overflow: hidden; margin-bottom: 8px; border: 1px solid var(--rs-border); } .cover-image img { width: 100%; height: 100%; object-fit: cover; } .footer { display: flex; align-items: center; justify-content: space-between; padding: 8px 14px; background: var(--rs-bg-surface-raised); border-top: 1px solid var(--rs-border); border-radius: 0 0 12px 12px; } .open-link { font-size: 11px; color: var(--rs-primary); cursor: pointer; text-decoration: none; font-weight: 500; } .open-link:hover { text-decoration: underline; } .status-badge { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; padding: 3px 8px; border-radius: 10px; } .status-badge.draft { background: var(--rs-bg-surface-raised); color: var(--rs-text-muted); } .status-badge.ready { background: rgba(59, 130, 246, 0.15); color: #2563eb; } .status-badge.published { background: rgba(34, 197, 94, 0.15); color: var(--rs-success); } `; declare global { interface HTMLElementTagNameMap { "folk-social-thread": FolkSocialThread; } } export class FolkSocialThread extends FolkShape { static override tagName = "folk-social-thread"; 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; } #threadId = ""; #title = "Untitled Thread"; #tweets: string[] = []; #platform: SocialPlatform = "x"; #status: "draft" | "ready" | "published" = "draft"; #imageUrl = ""; #spaceSlug = ""; #tweetCountEl: HTMLElement | null = null; #tweetPreviewEl: HTMLElement | null = null; #statusBadgeEl: HTMLElement | null = null; #coverImageEl: HTMLElement | null = null; get threadId() { return this.#threadId; } set threadId(v: string) { this.#threadId = v; this.#dispatchChange(); } get title() { return this.#title; } set title(v: string) { this.#title = v; this.requestUpdate("title"); this.#dispatchChange(); } get tweets(): string[] { return this.#tweets; } set tweets(v: string[]) { this.#tweets = v; if (this.#tweetCountEl) this.#tweetCountEl.textContent = `${v.length} tweet${v.length !== 1 ? "s" : ""}`; if (this.#tweetPreviewEl) this.#tweetPreviewEl.textContent = v[0] || "No tweets yet..."; this.requestUpdate("tweets"); this.#dispatchChange(); } get platform(): SocialPlatform { return this.#platform; } set platform(v: SocialPlatform) { this.#platform = v; this.requestUpdate("platform"); this.#dispatchChange(); } get status() { return this.#status; } set status(v: "draft" | "ready" | "published") { this.#status = v; if (this.#statusBadgeEl) { this.#statusBadgeEl.className = `status-badge ${v}`; this.#statusBadgeEl.textContent = v; } this.requestUpdate("status"); this.#dispatchChange(); } get imageUrl() { return this.#imageUrl; } set imageUrl(v: string) { this.#imageUrl = v; this.#renderCoverImage(); this.requestUpdate("imageUrl"); this.#dispatchChange(); } get spaceSlug() { return this.#spaceSlug; } set spaceSlug(v: string) { this.#spaceSlug = v; this.#dispatchChange(); } #dispatchChange() { this.dispatchEvent(new CustomEvent("content-change", { detail: this.toJSON() })); } override createRenderRoot() { const root = super.createRenderRoot(); const threadIdAttr = this.getAttribute("thread-id"); if (threadIdAttr) this.#threadId = threadIdAttr; const titleAttr = this.getAttribute("title"); if (titleAttr) this.#title = titleAttr; const platformAttr = this.getAttribute("platform") as SocialPlatform; if (platformAttr && platformAttr in PLATFORM_COLORS) this.#platform = platformAttr; const statusAttr = this.getAttribute("status") as any; if (statusAttr) this.#status = statusAttr; const color = PLATFORM_COLORS[this.#platform] || "#000"; const wrapper = document.createElement("div"); wrapper.style.position = "relative"; wrapper.style.height = "100%"; const escTitle = this.#escapeHtml(this.#title); const tweetCount = this.#tweets.length; const firstTweet = this.#escapeHtml(this.#tweets[0] || "No tweets yet..."); wrapper.innerHTML = html`
🧵 ${escTitle}
${tweetCount} tweet${tweetCount !== 1 ? "s" : ""}
${firstTweet}
${this.#imageUrl ? `Thread cover` : ""}
`; const slot = root.querySelector("slot"); const containerDiv = slot?.parentElement as HTMLElement; if (containerDiv) containerDiv.replaceWith(wrapper); this.#tweetCountEl = wrapper.querySelector(".tweet-count"); this.#tweetPreviewEl = wrapper.querySelector(".tweet-preview"); this.#statusBadgeEl = wrapper.querySelector(".status-badge"); this.#coverImageEl = wrapper.querySelector(".cover-image"); const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement; closeBtn.addEventListener("click", (e) => { e.stopPropagation(); this.dispatchEvent(new CustomEvent("close")); }); const openLink = wrapper.querySelector(".open-link") as HTMLElement; openLink.addEventListener("click", (e) => { e.stopPropagation(); this.dispatchEvent(new CustomEvent("navigate-to-module", { bubbles: true, composed: true, detail: { path: `/${this.#spaceSlug}/rsocials/thread-editor?id=${this.#threadId}` }, })); }); return root; } #renderCoverImage() { if (!this.#coverImageEl) return; if (this.#imageUrl) { this.#coverImageEl.style.display = "block"; this.#coverImageEl.innerHTML = `Thread cover`; } else { this.#coverImageEl.style.display = "none"; this.#coverImageEl.innerHTML = ""; } } #escapeHtml(text: string): string { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } static override fromData(data: Record): FolkSocialThread { const shape = FolkShape.fromData(data) as FolkSocialThread; if (data.threadId) shape.threadId = data.threadId; if (data.title) shape.title = data.title; if (Array.isArray(data.tweets)) shape.tweets = data.tweets; if (data.platform) shape.platform = data.platform; if (data.status) shape.status = data.status; if (data.imageUrl !== undefined) shape.imageUrl = data.imageUrl; if (data.spaceSlug) shape.spaceSlug = data.spaceSlug; return shape; } override applyData(data: Record): void { super.applyData(data); if (data.threadId !== undefined && data.threadId !== this.threadId) this.threadId = data.threadId; if (data.title !== undefined && data.title !== this.title) this.title = data.title; if (Array.isArray(data.tweets) && JSON.stringify(data.tweets) !== JSON.stringify(this.tweets)) this.tweets = data.tweets; if (data.platform !== undefined && data.platform !== this.platform) this.platform = data.platform; if (data.status !== undefined && data.status !== this.status) this.status = data.status; if (data.imageUrl !== undefined && data.imageUrl !== this.imageUrl) this.imageUrl = data.imageUrl; if (data.spaceSlug !== undefined && data.spaceSlug !== this.spaceSlug) this.spaceSlug = data.spaceSlug; } override toJSON() { return { ...super.toJSON(), type: "folk-social-thread", threadId: this.#threadId, title: this.#title, tweets: this.#tweets, platform: this.#platform, status: this.#status, imageUrl: this.#imageUrl, spaceSlug: this.#spaceSlug, }; } }