import { FolkShape } from "./folk-shape"; import { css, html } from "./tags"; const styles = css` :host { background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); min-width: 280px; min-height: 140px; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } :host(:hover) { box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12); } :host([data-status="posted"]) { box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.4), 0 2px 12px rgba(0, 0, 0, 0.08); } :host([data-status="scheduled"]) { box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.4), 0 2px 12px rgba(0, 0, 0, 0.08); } :host([data-status="failed"]) { box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.4), 0 2px 12px rgba(0, 0, 0, 0.08); } .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; } .platform-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; } .platform-name { font-size: 13px; font-weight: 600; letter-spacing: 0.3px; } .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; } .post-type { display: inline-block; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; padding: 2px 8px; border-radius: 10px; background: #f1f5f9; color: #64748b; margin-bottom: 8px; } .content-preview { font-size: 13px; line-height: 1.5; color: #1e293b; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 8px; } .media-preview { width: 100%; height: 100px; border-radius: 8px; background: #f1f5f9; display: flex; align-items: center; justify-content: center; margin-bottom: 8px; overflow: hidden; border: 1px solid #e2e8f0; } .media-preview img { width: 100%; height: 100%; object-fit: cover; } .media-placeholder { color: #94a3b8; font-size: 12px; display: flex; flex-direction: column; align-items: center; gap: 4px; } .media-placeholder .icon { font-size: 24px; } .hashtags { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px; } .hashtag { font-size: 11px; color: #3b82f6; background: #eff6ff; padding: 2px 6px; border-radius: 4px; } .footer { display: flex; align-items: center; justify-content: space-between; padding: 8px 14px; background: #f8fafc; border-top: 1px solid #e2e8f0; border-radius: 0 0 12px 12px; } .schedule { display: flex; align-items: center; gap: 6px; font-size: 11px; color: #64748b; } .schedule-icon { font-size: 13px; } .schedule-time { font-weight: 500; color: #475569; } .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: #f1f5f9; color: #64748b; } .status-badge.scheduled { background: #dbeafe; color: #2563eb; } .status-badge.posted { background: #dcfce7; color: #16a34a; } .status-badge.failed { background: #fee2e2; color: #dc2626; } .step-number { position: absolute; top: -8px; left: -8px; width: 24px; height: 24px; border-radius: 50%; background: #1e293b; color: white; font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; z-index: 1; border: 2px solid white; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); } .edit-overlay { display: none; position: absolute; inset: 0; background: white; z-index: 10; border-radius: 12px; flex-direction: column; } .edit-overlay.active { display: flex; } .edit-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid #e2e8f0; } .edit-header span { font-size: 13px; font-weight: 600; color: #1e293b; } .edit-body { flex: 1; padding: 12px 14px; overflow-y: auto; } .edit-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; } .edit-field label { font-size: 11px; font-weight: 500; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; } .edit-field input, .edit-field textarea, .edit-field select { padding: 8px 10px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 13px; outline: none; font-family: inherit; } .edit-field input:focus, .edit-field textarea:focus, .edit-field select:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } .edit-field textarea { min-height: 80px; resize: vertical; } .edit-actions { display: flex; gap: 8px; padding: 10px 14px; border-top: 1px solid #e2e8f0; } .edit-actions button { flex: 1; padding: 8px; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; border: 1px solid #e2e8f0; } .edit-actions .save-btn { background: #3b82f6; color: white; border-color: #3b82f6; } .edit-actions .save-btn:hover { background: #2563eb; } .edit-actions .cancel-btn { background: white; color: #64748b; } .edit-actions .cancel-btn:hover { background: #f1f5f9; } `; export type SocialPlatform = | "x" | "linkedin" | "instagram" | "youtube" | "threads" | "bluesky" | "tiktok" | "facebook"; export type PostStatus = "draft" | "scheduled" | "posted" | "failed"; export type PostType = | "text" | "image" | "video" | "carousel" | "story" | "reel" | "thread" | "article" | "short"; const PLATFORM_CONFIG: Record< SocialPlatform, { icon: string; label: string; color: string } > = { x: { icon: "\ud835\udd4f", label: "X", color: "#000000" }, linkedin: { icon: "in", label: "LinkedIn", color: "#0A66C2" }, instagram: { icon: "\ud83d\udcf7", label: "Instagram", color: "#E4405F" }, youtube: { icon: "\u25b6", label: "YouTube", color: "#FF0000" }, threads: { icon: "@", label: "Threads", color: "#000000" }, bluesky: { icon: "\ud83e\ude77", label: "Bluesky", color: "#0085FF" }, tiktok: { icon: "\u266b", label: "TikTok", color: "#010101" }, facebook: { icon: "f", label: "Facebook", color: "#1877F2" }, }; declare global { interface HTMLElementTagNameMap { "folk-social-post": FolkSocialPost; } } export class FolkSocialPost extends FolkShape { static override tagName = "folk-social-post"; 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; } #platform: SocialPlatform = "x"; #postType: PostType = "text"; #content = ""; #mediaUrl = ""; #mediaType = ""; // "image", "video", "carousel" #scheduledAt = ""; #status: PostStatus = "draft"; #hashtags: string[] = []; #stepNumber = 0; #isEditing = false; // DOM references #contentPreviewEl: HTMLElement | null = null; #statusBadgeEl: HTMLElement | null = null; #scheduleTimeEl: HTMLElement | null = null; #editOverlay: HTMLElement | null = null; #mediaPreviewEl: HTMLElement | null = null; #hashtagsEl: HTMLElement | null = null; #postTypeEl: HTMLElement | null = null; #stepNumberEl: HTMLElement | null = null; get platform() { return this.#platform; } set platform(value: SocialPlatform) { this.#platform = value; this.requestUpdate("platform"); this.#dispatchChange(); } get postType() { return this.#postType; } set postType(value: PostType) { this.#postType = value; if (this.#postTypeEl) this.#postTypeEl.textContent = value; this.requestUpdate("postType"); this.#dispatchChange(); } get content() { return this.#content; } set content(value: string) { this.#content = value; if (this.#contentPreviewEl) this.#contentPreviewEl.textContent = value; this.requestUpdate("content"); this.#dispatchChange(); } get mediaUrl() { return this.#mediaUrl; } set mediaUrl(value: string) { this.#mediaUrl = value; this.#renderMedia(); this.requestUpdate("mediaUrl"); this.#dispatchChange(); } get mediaType() { return this.#mediaType; } set mediaType(value: string) { this.#mediaType = value; this.#renderMedia(); this.requestUpdate("mediaType"); this.#dispatchChange(); } get scheduledAt() { return this.#scheduledAt; } set scheduledAt(value: string) { this.#scheduledAt = value; if (this.#scheduleTimeEl) this.#scheduleTimeEl.textContent = this.#formatSchedule(value); this.requestUpdate("scheduledAt"); this.#dispatchChange(); } get status(): PostStatus { return this.#status; } set status(value: PostStatus) { this.#status = value; this.setAttribute("data-status", value); if (this.#statusBadgeEl) { this.#statusBadgeEl.className = `status-badge ${value}`; this.#statusBadgeEl.textContent = value; } this.requestUpdate("status"); this.#dispatchChange(); } get hashtags(): string[] { return this.#hashtags; } set hashtags(value: string[]) { this.#hashtags = value; this.#renderHashtags(); this.requestUpdate("hashtags"); this.#dispatchChange(); } get stepNumber(): number { return this.#stepNumber; } set stepNumber(value: number) { this.#stepNumber = value; if (this.#stepNumberEl) { this.#stepNumberEl.textContent = String(value); this.#stepNumberEl.style.display = value > 0 ? "flex" : "none"; } this.requestUpdate("stepNumber"); this.#dispatchChange(); } #dispatchChange() { this.dispatchEvent( new CustomEvent("content-change", { detail: this.toJSON(), }), ); } override createRenderRoot() { const root = super.createRenderRoot(); // Parse attributes const platformAttr = this.getAttribute("platform") as SocialPlatform; if (platformAttr && platformAttr in PLATFORM_CONFIG) this.#platform = platformAttr; const postTypeAttr = this.getAttribute("post-type") as PostType; if (postTypeAttr) this.#postType = postTypeAttr; const contentAttr = this.getAttribute("content"); if (contentAttr) this.#content = contentAttr; const statusAttr = this.getAttribute("status") as PostStatus; if (statusAttr) this.#status = statusAttr; const stepAttr = this.getAttribute("step"); if (stepAttr) this.#stepNumber = parseInt(stepAttr, 10); const config = PLATFORM_CONFIG[this.#platform]; const wrapper = document.createElement("div"); wrapper.style.position = "relative"; wrapper.style.height = "100%"; wrapper.innerHTML = html` ${this.#stepNumber}