diff --git a/lib/canvas-tools.ts b/lib/canvas-tools.ts index 8e6579d..a4d8dad 100644 --- a/lib/canvas-tools.ts +++ b/lib/canvas-tools.ts @@ -281,6 +281,113 @@ const registry: CanvasToolDefinition[] = [ }, ]; +// ── Social Media / Campaign Tools ── +registry.push( + { + declaration: { + name: "create_social_post", + description: "Create a social media post card for scheduling across platforms.", + parameters: { + type: "object", + properties: { + platform: { type: "string", description: "Target platform", enum: ["x", "linkedin", "instagram", "youtube", "threads", "bluesky", "tiktok", "facebook"] }, + content: { type: "string", description: "Post text content" }, + postType: { type: "string", description: "Format", enum: ["text", "image", "video", "carousel", "thread", "article"] }, + scheduledAt: { type: "string", description: "ISO datetime to schedule" }, + hashtags: { type: "string", description: "Comma-separated hashtags" }, + }, + required: ["platform", "content"], + }, + }, + tagName: "folk-social-post", + buildProps: (args) => ({ + platform: args.platform || "x", + content: args.content, + postType: args.postType || "text", + scheduledAt: args.scheduledAt || "", + hashtags: args.hashtags ? args.hashtags.split(",").map((t: string) => t.trim()).filter(Boolean) : [], + status: "draft", + }), + actionLabel: (args) => `Created ${args.platform || "social"} post`, + }, + { + declaration: { + name: "create_social_thread", + description: "Create a tweet thread card on the canvas. Use when the user wants to draft a multi-tweet thread.", + parameters: { + type: "object", + properties: { + title: { type: "string", description: "Thread title" }, + platform: { type: "string", description: "Target platform", enum: ["x", "bluesky", "threads"] }, + tweetsJson: { type: "string", description: "JSON array of tweet strings" }, + status: { type: "string", description: "Thread status", enum: ["draft", "ready", "published"] }, + }, + required: ["title"], + }, + }, + tagName: "folk-social-thread", + buildProps: (args) => { + let tweets: string[] = []; + try { tweets = JSON.parse(args.tweetsJson || "[]"); } catch { tweets = []; } + return { + title: args.title, + platform: args.platform || "x", + tweets, + status: args.status || "draft", + }; + }, + actionLabel: (args) => `Created thread: ${args.title}`, + }, + { + declaration: { + name: "create_campaign_card", + description: "Create a campaign dashboard card on the canvas. Use when the user wants to plan or track a social media campaign.", + parameters: { + type: "object", + properties: { + title: { type: "string", description: "Campaign title" }, + description: { type: "string", description: "Campaign description" }, + platforms: { type: "string", description: "Comma-separated platform names" }, + duration: { type: "string", description: "Campaign duration (e.g. '4 weeks')" }, + }, + required: ["title"], + }, + }, + tagName: "folk-social-campaign", + buildProps: (args) => ({ + title: args.title, + description: args.description || "", + platforms: args.platforms ? args.platforms.split(",").map((p: string) => p.trim().toLowerCase()).filter(Boolean) : [], + duration: args.duration || "", + }), + actionLabel: (args) => `Created campaign: ${args.title}`, + }, + { + declaration: { + name: "create_newsletter_card", + description: "Create a newsletter/email campaign card on the canvas. Use when the user wants to draft or schedule an email newsletter.", + parameters: { + type: "object", + properties: { + subject: { type: "string", description: "Email subject line" }, + listName: { type: "string", description: "Mailing list name" }, + status: { type: "string", description: "Newsletter status", enum: ["draft", "scheduled", "sent"] }, + scheduledAt: { type: "string", description: "ISO datetime to schedule" }, + }, + required: ["subject"], + }, + }, + tagName: "folk-social-newsletter", + buildProps: (args) => ({ + subject: args.subject, + listName: args.listName || "", + status: args.status || "draft", + scheduledAt: args.scheduledAt || "", + }), + actionLabel: (args) => `Created newsletter: ${args.subject}`, + }, +); + // ── Design Agent Tool ── registry.push({ declaration: { diff --git a/lib/folk-social-campaign.ts b/lib/folk-social-campaign.ts new file mode 100644 index 0000000..da61643 --- /dev/null +++ b/lib/folk-social-campaign.ts @@ -0,0 +1,395 @@ +import { FolkShape } from "./folk-shape"; +import { css, html } from "./tags"; + +const PLATFORM_ICONS: Record = { + x: "𝕏", linkedin: "in", instagram: "📷", youtube: "▶", + threads: "@", bluesky: "🩷", tiktok: "♫", facebook: "f", +}; + +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: 280px; + min-height: 140px; + 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; + background: linear-gradient(135deg, #6366f1, #8b5cf6); + } + + .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; + } + + .description { + font-size: 13px; + line-height: 1.4; + color: var(--rs-text-secondary); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + margin-bottom: 10px; + } + + .platform-row { + display: flex; + gap: 6px; + margin-bottom: 10px; + flex-wrap: wrap; + } + + .platform-chip { + font-size: 12px; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + background: var(--rs-bg-surface-raised); + border-radius: 6px; + border: 1px solid var(--rs-border); + } + + .progress-section { + margin-bottom: 10px; + } + + .progress-label { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--rs-text-muted); + margin-bottom: 4px; + } + + .progress-bar { + height: 6px; + background: var(--rs-bg-surface-raised); + border-radius: 3px; + overflow: hidden; + } + + .progress-fill { + height: 100%; + background: linear-gradient(90deg, #6366f1, #8b5cf6); + border-radius: 3px; + transition: width 0.3s ease; + } + + .stats-row { + display: flex; + gap: 12px; + } + + .stat { + display: flex; + align-items: center; + gap: 4px; + font-size: 11px; + color: var(--rs-text-muted); + } + + .stat-count { + font-weight: 600; + color: var(--rs-text-primary); + } + + .stat-dot { + width: 6px; + height: 6px; + border-radius: 50%; + } + + .stat-dot.draft { background: #94a3b8; } + .stat-dot.scheduled { background: #3b82f6; } + .stat-dot.published { background: #22c55e; } + + .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; + } + + .duration-label { + font-size: 11px; + color: var(--rs-text-muted); + font-weight: 500; + } + + .open-link { + font-size: 11px; + color: var(--rs-primary); + cursor: pointer; + text-decoration: none; + font-weight: 500; + } + + .open-link:hover { + text-decoration: underline; + } +`; + +declare global { + interface HTMLElementTagNameMap { + "folk-social-campaign": FolkSocialCampaign; + } +} + +export class FolkSocialCampaign extends FolkShape { + static override tagName = "folk-social-campaign"; + + 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; + } + + #campaignId = ""; + #title = "Untitled Campaign"; + #description = ""; + #platforms: string[] = []; + #phases: { name: string; label: string; days: string }[] = []; + #postCount = 0; + #draftCount = 0; + #scheduledCount = 0; + #publishedCount = 0; + #duration = ""; + #spaceSlug = ""; + + get campaignId() { return this.#campaignId; } + set campaignId(v: string) { this.#campaignId = v; this.#dispatchChange(); } + + get title() { return this.#title; } + set title(v: string) { this.#title = v; this.requestUpdate("title"); this.#dispatchChange(); } + + get description() { return this.#description; } + set description(v: string) { this.#description = v; this.requestUpdate("description"); this.#dispatchChange(); } + + get platforms(): string[] { return this.#platforms; } + set platforms(v: string[]) { this.#platforms = v; this.requestUpdate("platforms"); this.#dispatchChange(); } + + get phases() { return this.#phases; } + set phases(v: { name: string; label: string; days: string }[]) { this.#phases = v; this.requestUpdate("phases"); this.#dispatchChange(); } + + get postCount() { return this.#postCount; } + set postCount(v: number) { this.#postCount = v; this.requestUpdate("postCount"); this.#dispatchChange(); } + + get draftCount() { return this.#draftCount; } + set draftCount(v: number) { this.#draftCount = v; this.requestUpdate("draftCount"); this.#dispatchChange(); } + + get scheduledCount() { return this.#scheduledCount; } + set scheduledCount(v: number) { this.#scheduledCount = v; this.requestUpdate("scheduledCount"); this.#dispatchChange(); } + + get publishedCount() { return this.#publishedCount; } + set publishedCount(v: number) { this.#publishedCount = v; this.requestUpdate("publishedCount"); this.#dispatchChange(); } + + get duration() { return this.#duration; } + set duration(v: string) { this.#duration = v; this.requestUpdate("duration"); 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 titleAttr = this.getAttribute("title"); + if (titleAttr) this.#title = titleAttr; + + const total = this.#postCount || 1; + const publishedPct = Math.round((this.#publishedCount / total) * 100); + + const escTitle = this.#escapeHtml(this.#title); + const escDesc = this.#escapeHtml(this.#description || "No description"); + + const platformChips = this.#platforms + .map((p) => `${PLATFORM_ICONS[p] || p[0]?.toUpperCase() || "?"}`) + .join(""); + + const wrapper = document.createElement("div"); + wrapper.style.position = "relative"; + wrapper.style.height = "100%"; + + wrapper.innerHTML = html` +
+
+ 📢 + ${escTitle} +
+
+ +
+
+
+
${escDesc}
+
${platformChips || 'No platforms'}
+
+
Phase Progress
+
+
+
+
${this.#draftCount} draft
+
${this.#scheduledCount} sched
+
${this.#publishedCount} pub
+
+
+ + `; + + const slot = root.querySelector("slot"); + const containerDiv = slot?.parentElement as HTMLElement; + if (containerDiv) containerDiv.replaceWith(wrapper); + + 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/campaigns?id=${this.#campaignId}` }, + })); + }); + + return root; + } + + #escapeHtml(text: string): string { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } + + static override fromData(data: Record): FolkSocialCampaign { + const shape = FolkShape.fromData(data) as FolkSocialCampaign; + if (data.campaignId) shape.campaignId = data.campaignId; + if (data.title) shape.title = data.title; + if (data.description !== undefined) shape.description = data.description; + if (Array.isArray(data.platforms)) shape.platforms = data.platforms; + if (Array.isArray(data.phases)) shape.phases = data.phases; + if (typeof data.postCount === "number") shape.postCount = data.postCount; + if (typeof data.draftCount === "number") shape.draftCount = data.draftCount; + if (typeof data.scheduledCount === "number") shape.scheduledCount = data.scheduledCount; + if (typeof data.publishedCount === "number") shape.publishedCount = data.publishedCount; + if (data.duration !== undefined) shape.duration = data.duration; + if (data.spaceSlug) shape.spaceSlug = data.spaceSlug; + return shape; + } + + override applyData(data: Record): void { + super.applyData(data); + if (data.campaignId !== undefined && data.campaignId !== this.campaignId) this.campaignId = data.campaignId; + if (data.title !== undefined && data.title !== this.title) this.title = data.title; + if (data.description !== undefined && data.description !== this.description) this.description = data.description; + if (Array.isArray(data.platforms) && JSON.stringify(data.platforms) !== JSON.stringify(this.platforms)) this.platforms = data.platforms; + if (Array.isArray(data.phases) && JSON.stringify(data.phases) !== JSON.stringify(this.phases)) this.phases = data.phases; + if (typeof data.postCount === "number" && data.postCount !== this.postCount) this.postCount = data.postCount; + if (typeof data.draftCount === "number" && data.draftCount !== this.draftCount) this.draftCount = data.draftCount; + if (typeof data.scheduledCount === "number" && data.scheduledCount !== this.scheduledCount) this.scheduledCount = data.scheduledCount; + if (typeof data.publishedCount === "number" && data.publishedCount !== this.publishedCount) this.publishedCount = data.publishedCount; + if (data.duration !== undefined && data.duration !== this.duration) this.duration = data.duration; + if (data.spaceSlug !== undefined && data.spaceSlug !== this.spaceSlug) this.spaceSlug = data.spaceSlug; + } + + override toJSON() { + return { + ...super.toJSON(), + type: "folk-social-campaign", + campaignId: this.#campaignId, + title: this.#title, + description: this.#description, + platforms: this.#platforms, + phases: this.#phases, + postCount: this.#postCount, + draftCount: this.#draftCount, + scheduledCount: this.#scheduledCount, + publishedCount: this.#publishedCount, + duration: this.#duration, + spaceSlug: this.#spaceSlug, + }; + } +} diff --git a/lib/folk-social-newsletter.ts b/lib/folk-social-newsletter.ts new file mode 100644 index 0000000..f524956 --- /dev/null +++ b/lib/folk-social-newsletter.ts @@ -0,0 +1,354 @@ +import { FolkShape } from "./folk-shape"; +import { css, html } from "./tags"; + +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; + background: linear-gradient(135deg, #7c3aed, #a855f7); + } + + .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-label { + 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; + } + + .subject-line { + font-size: 14px; + font-weight: 600; + color: var(--rs-text-primary); + margin-bottom: 8px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .list-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + } + + .list-name { + font-size: 12px; + color: var(--rs-text-secondary); + } + + .subscriber-badge { + font-size: 10px; + font-weight: 600; + padding: 2px 8px; + border-radius: 10px; + background: rgba(124, 58, 237, 0.1); + color: #7c3aed; + } + + .body-preview { + font-size: 13px; + line-height: 1.4; + color: var(--rs-text-secondary); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .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; + } + + .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.scheduled { + background: rgba(59, 130, 246, 0.15); + color: #2563eb; + } + + .status-badge.sent { + background: rgba(34, 197, 94, 0.15); + color: var(--rs-success); + } + + .schedule-info { + display: flex; + align-items: center; + gap: 4px; + font-size: 11px; + color: var(--rs-text-muted); + } +`; + +declare global { + interface HTMLElementTagNameMap { + "folk-social-newsletter": FolkSocialNewsletter; + } +} + +export class FolkSocialNewsletter extends FolkShape { + static override tagName = "folk-social-newsletter"; + + 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; + } + + #newsletterId = ""; + #subject = "Untitled Newsletter"; + #listName = ""; + #subscriberCount = 0; + #status: "draft" | "scheduled" | "sent" = "draft"; + #scheduledAt = ""; + #bodyPreview = ""; + #spaceSlug = ""; + + #subjectEl: HTMLElement | null = null; + #statusBadgeEl: HTMLElement | null = null; + #scheduleInfoEl: HTMLElement | null = null; + + get newsletterId() { return this.#newsletterId; } + set newsletterId(v: string) { this.#newsletterId = v; this.#dispatchChange(); } + + get subject() { return this.#subject; } + set subject(v: string) { + this.#subject = v; + if (this.#subjectEl) this.#subjectEl.textContent = v; + this.requestUpdate("subject"); + this.#dispatchChange(); + } + + get listName() { return this.#listName; } + set listName(v: string) { this.#listName = v; this.requestUpdate("listName"); this.#dispatchChange(); } + + get subscriberCount() { return this.#subscriberCount; } + set subscriberCount(v: number) { this.#subscriberCount = v; this.requestUpdate("subscriberCount"); this.#dispatchChange(); } + + get status() { return this.#status; } + set status(v: "draft" | "scheduled" | "sent") { + this.#status = v; + if (this.#statusBadgeEl) { + this.#statusBadgeEl.className = `status-badge ${v}`; + this.#statusBadgeEl.textContent = v; + } + this.requestUpdate("status"); + this.#dispatchChange(); + } + + get scheduledAt() { return this.#scheduledAt; } + set scheduledAt(v: string) { + this.#scheduledAt = v; + if (this.#scheduleInfoEl) this.#scheduleInfoEl.textContent = this.#formatSchedule(v); + this.requestUpdate("scheduledAt"); + this.#dispatchChange(); + } + + get bodyPreview() { return this.#bodyPreview; } + set bodyPreview(v: string) { this.#bodyPreview = v; this.requestUpdate("bodyPreview"); 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 subjectAttr = this.getAttribute("subject"); + if (subjectAttr) this.#subject = subjectAttr; + const statusAttr = this.getAttribute("status") as any; + if (statusAttr) this.#status = statusAttr; + + const escSubject = this.#escapeHtml(this.#subject); + const escListName = this.#escapeHtml(this.#listName || "No list"); + const escPreview = this.#escapeHtml(this.#bodyPreview || "No content preview"); + + const wrapper = document.createElement("div"); + wrapper.style.position = "relative"; + wrapper.style.height = "100%"; + + wrapper.innerHTML = html` +
+
+ 📧 + Newsletter +
+
+ +
+
+
+
${escSubject}
+
+ ${escListName} + ${this.#subscriberCount} subscribers +
+
${escPreview}
+
+ + `; + + const slot = root.querySelector("slot"); + const containerDiv = slot?.parentElement as HTMLElement; + if (containerDiv) containerDiv.replaceWith(wrapper); + + this.#subjectEl = wrapper.querySelector(".subject-line"); + this.#statusBadgeEl = wrapper.querySelector(".status-badge"); + this.#scheduleInfoEl = wrapper.querySelector(".schedule-info"); + + const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement; + closeBtn.addEventListener("click", (e) => { + e.stopPropagation(); + this.dispatchEvent(new CustomEvent("close")); + }); + + return root; + } + + #formatSchedule(dateStr: string): string { + if (!dateStr) return "Not scheduled"; + try { + const date = new Date(dateStr); + if (isNaN(date.getTime())) return dateStr; + return date.toLocaleDateString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); + } catch { + return dateStr; + } + } + + #escapeHtml(text: string): string { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } + + static override fromData(data: Record): FolkSocialNewsletter { + const shape = FolkShape.fromData(data) as FolkSocialNewsletter; + if (data.newsletterId) shape.newsletterId = data.newsletterId; + if (data.subject) shape.subject = data.subject; + if (data.listName !== undefined) shape.listName = data.listName; + if (typeof data.subscriberCount === "number") shape.subscriberCount = data.subscriberCount; + if (data.status) shape.status = data.status; + if (data.scheduledAt !== undefined) shape.scheduledAt = data.scheduledAt; + if (data.bodyPreview !== undefined) shape.bodyPreview = data.bodyPreview; + if (data.spaceSlug) shape.spaceSlug = data.spaceSlug; + return shape; + } + + override applyData(data: Record): void { + super.applyData(data); + if (data.newsletterId !== undefined && data.newsletterId !== this.newsletterId) this.newsletterId = data.newsletterId; + if (data.subject !== undefined && data.subject !== this.subject) this.subject = data.subject; + if (data.listName !== undefined && data.listName !== this.listName) this.listName = data.listName; + if (typeof data.subscriberCount === "number" && data.subscriberCount !== this.subscriberCount) this.subscriberCount = data.subscriberCount; + if (data.status !== undefined && data.status !== this.status) this.status = data.status; + if (data.scheduledAt !== undefined && data.scheduledAt !== this.scheduledAt) this.scheduledAt = data.scheduledAt; + if (data.bodyPreview !== undefined && data.bodyPreview !== this.bodyPreview) this.bodyPreview = data.bodyPreview; + if (data.spaceSlug !== undefined && data.spaceSlug !== this.spaceSlug) this.spaceSlug = data.spaceSlug; + } + + override toJSON() { + return { + ...super.toJSON(), + type: "folk-social-newsletter", + newsletterId: this.#newsletterId, + subject: this.#subject, + listName: this.#listName, + subscriberCount: this.#subscriberCount, + status: this.#status, + scheduledAt: this.#scheduledAt, + bodyPreview: this.#bodyPreview, + spaceSlug: this.#spaceSlug, + }; + } +} diff --git a/lib/folk-social-thread.ts b/lib/folk-social-thread.ts new file mode 100644 index 0000000..3289310 --- /dev/null +++ b/lib/folk-social-thread.ts @@ -0,0 +1,394 @@ +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, + }; + } +} diff --git a/lib/index.ts b/lib/index.ts index e6011ed..1284ff5 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -72,6 +72,9 @@ export * from "./folk-transaction-builder"; // Social Media / Campaign Shapes export * from "./folk-social-post"; +export * from "./folk-social-thread"; +export * from "./folk-social-campaign"; +export * from "./folk-social-newsletter"; // Decision/Choice Shapes export * from "./folk-choice-vote"; diff --git a/lib/mi-suggestions.ts b/lib/mi-suggestions.ts new file mode 100644 index 0000000..ce78557 --- /dev/null +++ b/lib/mi-suggestions.ts @@ -0,0 +1,113 @@ +/** + * MI Context-Aware Suggestions — client-side suggestion registry. + * + * Maps module IDs to suggested prompts. When the MI panel opens, + * it reads the current module context and returns matching chips. + * No server call needed for static suggestions. + */ + +export interface MiSuggestion { + label: string; + icon: string; + prompt: string; + autoSend?: boolean; +} + +const MODULE_SUGGESTIONS: Record = { + rspace: [ + { label: "Add a note", icon: "📝", prompt: "Add a markdown note to the canvas", autoSend: true }, + { label: "Generate an image", icon: "🎨", prompt: "Generate an image", autoSend: false }, + { label: "Arrange my shapes", icon: "📐", prompt: "Arrange all shapes on the canvas into a grid", autoSend: true }, + { label: "What's on this canvas?", icon: "🔍", prompt: "Summarize what's on this canvas", autoSend: true }, + ], + rnotes: [ + { label: "Create a notebook", icon: "📓", prompt: "Create a new notebook", autoSend: true }, + { label: "Summarize my notes", icon: "📋", prompt: "Summarize my recent notes", autoSend: true }, + { label: "Import from Obsidian", icon: "⬇", prompt: "How do I import notes from Obsidian?", autoSend: true }, + ], + rcal: [ + { label: "Create an event", icon: "📅", prompt: "Create a calendar event", autoSend: false }, + { label: "What's this week?", icon: "🗓", prompt: "What's on my calendar this week?", autoSend: true }, + { label: "Import a calendar", icon: "⬇", prompt: "How do I import an ICS calendar?", autoSend: true }, + ], + rtasks: [ + { label: "Create a task", icon: "✅", prompt: "Create a task", autoSend: false }, + { label: "Show open tasks", icon: "📋", prompt: "Show my open tasks", autoSend: true }, + { label: "Set up a project", icon: "🏗", prompt: "Help me set up a project board", autoSend: true }, + ], + rflows: [ + { label: "Create a flow", icon: "💧", prompt: "Create a new funding flow", autoSend: false }, + { label: "How do flows work?", icon: "❓", prompt: "How do community funding flows work?", autoSend: true }, + ], + rvote: [ + { label: "Create a proposal", icon: "🗳", prompt: "Create a governance proposal", autoSend: false }, + { label: "Start a vote", icon: "✋", prompt: "Start a governance vote", autoSend: false }, + ], + rchat: [ + { label: "Start a discussion", icon: "💬", prompt: "Start a new discussion thread", autoSend: false }, + { label: "Create a channel", icon: "📢", prompt: "Create a new chat channel", autoSend: false }, + ], + rcrm: [ + { label: "Add a contact", icon: "👤", prompt: "Add a new contact", autoSend: false }, + { label: "Show my pipeline", icon: "📊", prompt: "Show my CRM pipeline", autoSend: true }, + ], + rsocials: [ + { label: "Create a campaign", icon: "📣", prompt: "Create a social media campaign", autoSend: false }, + { label: "Draft a post", icon: "✏", prompt: "Draft a social media post", autoSend: false }, + ], + rwallet: [ + { label: "Check my balance", icon: "💰", prompt: "Check my wallet balance", autoSend: true }, + { label: "How do tokens work?", icon: "🪙", prompt: "How do CRDT tokens work?", autoSend: true }, + ], + rmaps: [ + { label: "Share my location", icon: "📍", prompt: "How do I share my location?", autoSend: true }, + { label: "Find nearby places", icon: "🔍", prompt: "Find interesting places nearby", autoSend: true }, + ], + rphotos: [ + { label: "Upload photos", icon: "📷", prompt: "How do I upload photos?", autoSend: true }, + { label: "Create an album", icon: "🖼", prompt: "Create a new photo album", autoSend: false }, + ], + rfiles: [ + { label: "Upload a file", icon: "📁", prompt: "How do I upload files?", autoSend: true }, + { label: "Browse files", icon: "🗂", prompt: "Show my recent files", autoSend: true }, + ], +}; + +const GENERIC_SUGGESTIONS: MiSuggestion[] = [ + { label: "What can I do here?", icon: "✨", prompt: "What can I do in this space?", autoSend: true }, + { label: "Set up this space", icon: "🏗", prompt: "Help me set up this space", autoSend: true }, + { label: "Show me around", icon: "🧭", prompt: "Give me a tour of the available rApps", autoSend: true }, +]; + +const CANVAS_SUGGESTIONS: MiSuggestion[] = [ + { label: "Summarize selected", icon: "📋", prompt: "Summarize the selected shapes", autoSend: true }, + { label: "Connect these", icon: "🔗", prompt: "Connect the selected shapes", autoSend: true }, + { label: "Arrange in grid", icon: "📐", prompt: "Arrange the selected shapes in a grid", autoSend: true }, +]; + +export function getContextSuggestions(ctx: { + module: string; + hasShapes: boolean; + hasSelectedShapes: boolean; +}): MiSuggestion[] { + const suggestions: MiSuggestion[] = []; + + // Canvas selection suggestions take priority + if (ctx.hasSelectedShapes) { + suggestions.push(...CANVAS_SUGGESTIONS); + } + + // Module-specific suggestions + const moduleSuggestions = MODULE_SUGGESTIONS[ctx.module]; + if (moduleSuggestions) { + suggestions.push(...moduleSuggestions); + } + + // Always add generic suggestions if we have room + if (suggestions.length < 6) { + const remaining = 6 - suggestions.length; + suggestions.push(...GENERIC_SUGGESTIONS.slice(0, remaining)); + } + + return suggestions; +} diff --git a/modules/rsocials/components/folk-campaign-manager.ts b/modules/rsocials/components/folk-campaign-manager.ts index d93c5f7..852d365 100644 --- a/modules/rsocials/components/folk-campaign-manager.ts +++ b/modules/rsocials/components/folk-campaign-manager.ts @@ -117,13 +117,19 @@ export class FolkCampaignManager extends HTMLElement { const statusClass = post.status === 'scheduled' ? 'status--scheduled' : 'status--draft'; const date = new Date(post.scheduledAt); const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); - const contentPreview = post.content.length > 180 ? this.esc(post.content.substring(0, 180)) + '...' : this.esc(post.content); + const isLong = post.content.length > 180; + const contentPreview = isLong ? this.esc(post.content.substring(0, 180)) + '...' : this.esc(post.content); + const fullContent = this.esc(post.content); const tags = post.hashtags.map(h => `#${this.esc(h)}`).join(' '); - // Thread badge + // Thread badge — links to thread editor if threadId exists let threadBadge = ''; if (post.threadPosts && post.threadPosts.length > 0) { - threadBadge = `🧵 ${post.threadPosts.length}-post thread`; + if (post.threadId) { + threadBadge = `🧵 ${post.threadPosts.length}-post thread — Edit`; + } else { + threadBadge = `🧵 ${post.threadPosts.length}-post thread`; + } } // Email subject for newsletter @@ -141,8 +147,14 @@ export class FolkCampaignManager extends HTMLElement { threadExpansion = ``; } + // "Open Thread" link for posts with created threads + let threadLink = ''; + if (post.threadId && !(post.threadPosts && post.threadPosts.length > 0)) { + threadLink = `Open Thread`; + } + return ` -
+
${icon} ${emailLine}
Step ${post.stepNumber}
-

${contentPreview.replace(/\n/g, '
')}

+

${contentPreview.replace(/\n/g, '
')}

${threadBadge} ${threadExpansion} + ${threadLink}
`; } @@ -269,6 +282,17 @@ export class FolkCampaignManager extends HTMLElement { transition: background 0.15s; } .thread-badge:hover { background: rgba(96,165,250,0.25); } + .thread-badge--link { border: 1px solid rgba(96,165,250,0.3); text-decoration: none; } + .thread-badge--link:hover { border-color: #60a5fa; } + .post { cursor: pointer; } + .post__content[data-collapsed="false"] { display: block; -webkit-line-clamp: unset; overflow: visible; white-space: normal; } + .post__thread-link { + display: inline-block; font-size: 0.7rem; color: #14b8a6; text-decoration: none; + padding: 2px 8px; border-radius: 4px; margin-bottom: 0.4rem; + background: rgba(20,184,166,0.1); border: 1px solid rgba(20,184,166,0.2); + transition: background 0.15s, border-color 0.15s; + } + .post__thread-link:hover { background: rgba(20,184,166,0.2); border-color: #14b8a6; } .thread-expansion { margin: 0.4rem 0; padding: 0.5rem; background: var(--rs-input-bg, #0f172a); border-radius: 6px; border: 1px solid var(--rs-input-border, #334155); } .thread-expansion[hidden] { display: none; } .thread-post { font-size: 0.75rem; color: var(--rs-text-secondary, #94a3b8); padding: 0.3rem 0; border-bottom: 1px solid rgba(51,65,85,0.5); line-height: 1.4; } @@ -531,15 +555,42 @@ export class FolkCampaignManager extends HTMLElement { }); }); - // ── Thread badge toggles ── - this.shadowRoot.querySelectorAll('.thread-badge').forEach(badge => { - badge.addEventListener('click', () => { + // ── Thread badge toggles (skip linked badges — they navigate) ── + this.shadowRoot.querySelectorAll('.thread-badge:not(.thread-badge--link)').forEach(badge => { + badge.addEventListener('click', (e) => { + e.stopPropagation(); const postId = (badge as HTMLElement).dataset.postId; if (!postId) return; const expansion = this.shadowRoot!.getElementById(`thread-${postId}`); if (expansion) expansion.hidden = !expansion.hidden; }); }); + + // ── Linked thread badges (stop post card click from firing) ── + this.shadowRoot.querySelectorAll('.thread-badge--link').forEach(badge => { + badge.addEventListener('click', (e) => e.stopPropagation()); + }); + + // ── Thread link buttons (stop post card click) ── + this.shadowRoot.querySelectorAll('.post__thread-link').forEach(link => { + link.addEventListener('click', (e) => e.stopPropagation()); + }); + + // ── Post card click → toggle content expand ── + this.shadowRoot.querySelectorAll('.post').forEach(post => { + post.addEventListener('click', () => { + const content = post.querySelector('.post__content') as HTMLElement; + if (!content) return; + const collapsed = content.dataset.collapsed === 'true'; + if (collapsed) { + content.innerHTML = (content.dataset.full || '').replace(/\n/g, '
'); + content.dataset.collapsed = 'false'; + } else { + content.innerHTML = (content.dataset.preview || '').replace(/\n/g, '
'); + content.dataset.collapsed = 'true'; + } + }); + }); } private async handleGenerate() { diff --git a/modules/rsocials/components/folk-campaign-wizard.ts b/modules/rsocials/components/folk-campaign-wizard.ts index 3635771..09dbbe3 100644 --- a/modules/rsocials/components/folk-campaign-wizard.ts +++ b/modules/rsocials/components/folk-campaign-wizard.ts @@ -585,7 +585,12 @@ export class FolkCampaignWizard extends HTMLElement {
\u2705

Campaign Activated!

Your campaign has been committed successfully.

- ${result.threadIds?.length ? `

${result.threadIds.length} thread(s) created

` : ''} + ${result.threads?.length ? `
+

${result.threads.length} thread(s) created:

+
+ ${result.threads.map((t: any) => `\u270F\uFE0F ${t.title || t.id}`).join('')} +
+
` : result.threadIds?.length ? `

${result.threadIds.length} thread(s) created

` : ''} ${result.newsletters?.length ? `

${result.newsletters.length} newsletter draft(s)

` : ''} +
` : `
@@ -690,6 +691,7 @@ export class RStackSpaceSettings extends HTMLElement { const token = getToken(); if (!token) return; + const username = input.value; try { const res = await fetch(`/api/spaces/${this._space}/members/add`, { method: "POST", @@ -697,15 +699,24 @@ export class RStackSpaceSettings extends HTMLElement { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, - body: JSON.stringify({ username: input.value, role: roleSelect.value }), + body: JSON.stringify({ username, role: roleSelect.value }), }); if (res.ok) { input.value = ""; this._lookupResult = null; + this._lookupError = ""; + this._render(); + // Show invite-sent feedback briefly + const feedbackEl = sr.getElementById("username-invite-feedback"); + if (feedbackEl) { + feedbackEl.textContent = `Invite sent to ${username}`; + feedbackEl.style.color = "#14b8a6"; + setTimeout(() => { feedbackEl.textContent = ""; }, 3000); + } this._loadData(); } else { const err = await res.json(); - this._lookupError = err.error || "Failed to add member"; + this._lookupError = err.error || "Failed to invite member"; this._render(); } } catch { @@ -734,16 +745,10 @@ export class RStackSpaceSettings extends HTMLElement { body: JSON.stringify({ email: input.value, role: roleSelect.value }), }); if (res.ok) { - const data = await res.json() as { type?: string; username?: string }; input.value = ""; if (feedback) { - if (data.type === "direct-add") { - feedback.textContent = `${data.username || "User"} added`; - feedback.style.color = "#14b8a6"; - } else { - feedback.textContent = "Invite sent"; - feedback.style.color = "#14b8a6"; - } + feedback.textContent = "Invite sent"; + feedback.style.color = "#14b8a6"; setTimeout(() => { feedback.textContent = ""; }, 3000); } this._loadData();