${filter === 'all' ? 'No posts or threads yet.' : `No ${filter} posts.`} Create your first post or thread.
No ${filter} posts. Create your first post or thread.
Create Thread
Open Campaigns
@@ -205,10 +230,7 @@ export class FolkThreadGallery extends HTMLElement {
const statusClass = `badge badge--${p.status}`;
const statusLabel = p.status.charAt(0).toUpperCase() + p.status.slice(1);
const cardClass = `card card--${p.status}`;
- const href = p.threadId
- ? `${this.basePath}thread-editor/${this.esc(p.threadId)}/edit`
- : `${this.basePath}campaign`;
- return `
+ return `
${statusLabel}
${this.esc(p.campaignTitle)}
@@ -219,7 +241,7 @@ export class FolkThreadGallery extends HTMLElement {
${p.hashtags.length ? `${p.hashtags.slice(0, 3).join(' ')} ` : ''}
${schedDate ? `${schedDate} ` : ''}
- `;
+ `;
}).join('')}
${threadsVisible.map(t => {
const initial = (t.name || '?').charAt(0).toUpperCase();
@@ -269,8 +291,11 @@ export class FolkThreadGallery extends HTMLElement {
padding: 1.25rem; transition: border-color 0.15s, transform 0.15s;
display: flex; flex-direction: column; gap: 0.75rem;
text-decoration: none; color: inherit;
+ font: inherit; text-align: left; cursor: pointer;
+ width: 100%;
}
.card:hover { border-color: var(--rs-primary); transform: translateY(-2px); }
+ .card:focus-visible { outline: 2px solid #14b8a6; outline-offset: 2px; }
.card--draft { border-left: 3px solid #f59e0b; }
.card--scheduled { border-left: 3px solid #60a5fa; }
.card--published { border-left: 3px solid #22c55e; }
@@ -327,6 +352,121 @@ export class FolkThreadGallery extends HTMLElement {
}
.card__image { border-radius: 8px; overflow: hidden; border: 1px solid var(--rs-input-border, #334155); margin-bottom: 0.25rem; }
.card__image img { display: block; width: 100%; height: 120px; object-fit: cover; }
+
+ /* ── Post detail overlay ── */
+ .overlay-backdrop {
+ position: fixed; inset: 0; z-index: 1000;
+ background: rgba(8, 12, 24, 0.6);
+ backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
+ display: flex; align-items: center; justify-content: center;
+ padding: 2rem; animation: fadeIn 0.15s ease-out;
+ }
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
+ .overlay-card {
+ background: var(--rs-bg-surface, #1e293b);
+ border: 1px solid var(--rs-input-border, #334155);
+ border-radius: 14px;
+ box-shadow: 0 24px 80px -20px rgba(0, 0, 0, 0.6);
+ width: min(720px, 100%); max-height: calc(100vh - 4rem);
+ display: flex; flex-direction: column; overflow: hidden;
+ transform-origin: center center;
+ }
+ .overlay-header {
+ display: flex; align-items: center; gap: 0.75rem;
+ padding: 1rem 1.25rem; border-bottom: 1px solid var(--rs-input-border, #334155);
+ }
+ .overlay-platform {
+ font-size: 1.25rem; line-height: 1;
+ width: 2.25rem; height: 2.25rem; border-radius: 8px;
+ background: rgba(20, 184, 166, 0.12);
+ display: flex; align-items: center; justify-content: center;
+ }
+ .overlay-headline {
+ flex: 1; display: flex; flex-direction: column; gap: 0.15rem; min-width: 0;
+ }
+ .overlay-title {
+ font-size: 1rem; font-weight: 700; color: var(--rs-text-primary, #f1f5f9);
+ line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+ }
+ .overlay-sub {
+ display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
+ font-size: 0.75rem; color: var(--rs-text-muted, #64748b);
+ }
+ .overlay-close {
+ background: transparent; border: 1px solid var(--rs-input-border, #334155);
+ color: var(--rs-text-secondary, #94a3b8);
+ width: 2rem; height: 2rem; border-radius: 8px;
+ font-size: 1.1rem; line-height: 1; cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ transition: background 0.12s, color 0.12s;
+ }
+ .overlay-close:hover { background: rgba(239,68,68,0.1); color: #fca5a5; border-color: #ef4444; }
+ .overlay-body {
+ padding: 1.5rem 1.25rem;
+ overflow-y: auto;
+ display: flex; flex-direction: column; gap: 1rem;
+ }
+ .overlay-content {
+ font-size: 1rem; line-height: 1.6;
+ color: var(--rs-text-primary, #f1f5f9);
+ white-space: pre-wrap; word-wrap: break-word;
+ }
+ .overlay-meta-grid {
+ display: grid; grid-template-columns: auto 1fr; gap: 0.5rem 1rem;
+ font-size: 0.82rem;
+ }
+ .overlay-meta-label {
+ color: var(--rs-text-muted, #64748b);
+ text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.7rem; font-weight: 600;
+ padding-top: 0.25rem;
+ }
+ .overlay-meta-value { color: var(--rs-text-secondary, #94a3b8); }
+ .overlay-hashtags { display: flex; gap: 0.4rem; flex-wrap: wrap; }
+ .overlay-hashtags span {
+ background: rgba(99,102,241,0.15); color: #a5b4fc;
+ padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.72rem;
+ }
+ .overlay-footer {
+ display: flex; justify-content: flex-end; gap: 0.5rem;
+ padding: 0.85rem 1.25rem; border-top: 1px solid var(--rs-input-border, #334155);
+ }
+ .overlay-btn {
+ padding: 0.5rem 1rem; border-radius: 8px;
+ border: 1px solid var(--rs-input-border, #334155);
+ background: transparent; color: var(--rs-text-primary, #e2e8f0);
+ font-size: 0.85rem; font-weight: 600; cursor: pointer;
+ font-family: inherit;
+ transition: background 0.12s, border-color 0.12s;
+ }
+ .overlay-btn:hover { background: rgba(255,255,255,0.05); }
+ .overlay-btn--primary {
+ background: linear-gradient(135deg,#14b8a6,#0d9488); color: #fff; border-color: #0d9488;
+ }
+ .overlay-btn--primary:hover { filter: brightness(1.1); }
+ .overlay-btn--danger { border-color: #ef4444; color: #fca5a5; }
+ .overlay-btn--danger:hover { background: rgba(239,68,68,0.1); }
+ /* Edit-mode controls */
+ .edit-field { display: flex; flex-direction: column; gap: 0.3rem; }
+ .edit-label {
+ font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
+ color: var(--rs-text-muted, #64748b);
+ }
+ .edit-input, .edit-textarea, .edit-select {
+ background: var(--rs-bg-surface-raised, #0f172a);
+ border: 1px solid var(--rs-input-border, #334155);
+ border-radius: 8px; color: var(--rs-text-primary, #f1f5f9);
+ padding: 0.6rem 0.75rem; font-size: 0.9rem; font-family: inherit;
+ }
+ .edit-input:focus, .edit-textarea:focus, .edit-select:focus {
+ outline: none; border-color: #14b8a6;
+ box-shadow: 0 0 0 3px rgba(20,184,166,0.15);
+ }
+ .edit-textarea {
+ min-height: 120px; resize: vertical; line-height: 1.6;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ font-size: 0.9rem;
+ }
+ .edit-saving { font-size: 0.75rem; color: #94a3b8; margin-right: auto; align-self: center; }
+ ${this.renderOverlay()}
`;
// Attach chip click handlers after render
@@ -348,6 +489,268 @@ export class FolkThreadGallery extends HTMLElement {
this.render();
});
});
+
+ // Card click → expand
+ this.shadowRoot.querySelectorAll('button.card[data-post-id]').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ e.preventDefault();
+ const id = (btn as HTMLElement).dataset.postId;
+ if (id) this.expandPost(id, btn as HTMLElement);
+ });
+ });
+
+ // Overlay wiring
+ this.wireOverlay();
+ }
+
+ // ── Overlay rendering ──
+
+ private renderOverlay(): string {
+ if (!this._expandedPostId) return '';
+ const post = this._allPosts.find(p => p.id === this._expandedPostId);
+ if (!post) return '';
+
+ const platformLabel = post.platform.charAt(0).toUpperCase() + post.platform.slice(1);
+ const statusLabel = post.status.charAt(0).toUpperCase() + post.status.slice(1);
+ const schedDate = post.scheduledAt
+ ? new Date(post.scheduledAt).toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' })
+ : '—';
+
+ const header = ``;
+
+ let body = '';
+ let footer = '';
+
+ if (this._editMode && this._editDraft) {
+ const d = this._editDraft;
+ // datetime-local needs YYYY-MM-DDTHH:mm in local time
+ const dtLocal = d.scheduledAt ? toDatetimeLocal(d.scheduledAt) : '';
+ body = `
+
+ Content
+
+
+
+ Status
+
+ Draft
+ Scheduled
+ Published
+
+
+
+ Scheduled at
+
+
+
+ Hashtags (space separated)
+
+
+
`;
+ footer = ``;
+ } else {
+ body = `
+
${this.esc(post.content) || 'Empty post '}
+
+
`;
+ const openHref = post.threadId
+ ? `${this.basePath}thread-editor/${this.esc(post.threadId)}/edit`
+ : `${this.basePath}campaign`;
+ footer = ``;
+ }
+
+ return `
+
+ ${header}
+ ${body}
+ ${footer}
+
+
`;
+ }
+
+ private wireOverlay(): void {
+ if (!this.shadowRoot) return;
+ const backdrop = this.shadowRoot.querySelector('.overlay-backdrop') as HTMLElement | null;
+ if (!backdrop) return;
+
+ backdrop.addEventListener('click', (e) => {
+ const target = e.target as HTMLElement;
+ if (target.dataset.action === 'backdrop') {
+ if (this._editMode) this.exitEditMode();
+ else this.closeOverlay();
+ return;
+ }
+ const actionEl = target.closest
('[data-action]');
+ if (!actionEl) return;
+ const action = actionEl.dataset.action;
+ if (action === 'close') this.closeOverlay();
+ else if (action === 'edit') this.enterEditMode();
+ else if (action === 'cancel-edit') this.exitEditMode();
+ else if (action === 'save') this.saveEdit();
+ });
+
+ // Bind input listeners in edit mode so the draft stays fresh
+ if (this._editMode) {
+ backdrop.querySelectorAll('[data-field]').forEach(el => {
+ el.addEventListener('input', (e) => {
+ const input = e.currentTarget as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
+ const field = input.dataset.field;
+ if (!field || !this._editDraft) return;
+ (this._editDraft as any)[field] = input.value;
+ });
+ });
+ }
+
+ // FLIP animation entry (only on first mount of this overlay)
+ const card = backdrop.querySelector('#overlay-card') as HTMLElement | null;
+ if (card && !card.dataset.animated) {
+ card.dataset.animated = '1';
+ const origin = this._flipOrigin;
+ if (origin) {
+ const target = card.getBoundingClientRect();
+ const dx = origin.left + origin.width / 2 - (target.left + target.width / 2);
+ const dy = origin.top + origin.height / 2 - (target.top + target.height / 2);
+ const sx = origin.width / target.width;
+ const sy = origin.height / target.height;
+ card.animate([
+ { transform: `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`, opacity: 0.6 },
+ { transform: 'translate(0, 0) scale(1, 1)', opacity: 1 },
+ ], { duration: 220, easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)' });
+ }
+ }
+ }
+
+ // ── Overlay state transitions ──
+
+ private _flipOrigin: DOMRect | null = null;
+
+ private expandPost(id: string, sourceEl?: HTMLElement): void {
+ this._flipOrigin = sourceEl?.getBoundingClientRect() ?? null;
+ this._expandedPostId = id;
+ this._editMode = false;
+ this._editDraft = null;
+ this._savingIndicator = '';
+ this.render();
+ }
+
+ private closeOverlay(): void {
+ this._expandedPostId = null;
+ this._editMode = false;
+ this._editDraft = null;
+ this._flipOrigin = null;
+ this.render();
+ }
+
+ private enterEditMode(): void {
+ if (!this._expandedPostId) return;
+ if (this._isDemoFallback) return; // no runtime = read-only
+ const post = this._allPosts.find(p => p.id === this._expandedPostId);
+ if (!post) return;
+ this._editDraft = {
+ content: post.content,
+ scheduledAt: post.scheduledAt,
+ hashtags: (post.hashtags || []).join(' '),
+ status: post.status as 'draft' | 'scheduled' | 'published',
+ };
+ this._editMode = true;
+ this.render();
+ // Focus content textarea
+ requestAnimationFrame(() => {
+ const ta = this.shadowRoot?.getElementById('f-content') as HTMLTextAreaElement | null;
+ ta?.focus();
+ });
+ }
+
+ private exitEditMode(): void {
+ this._editMode = false;
+ this._editDraft = null;
+ this._savingIndicator = '';
+ this.render();
+ }
+
+ private async saveEdit(): Promise {
+ if (!this._editDraft || !this._expandedPostId) return;
+ const runtime = (window as any).__rspaceOfflineRuntime;
+ if (!runtime?.isInitialized) {
+ this._savingIndicator = 'Offline runtime unavailable';
+ this.render();
+ return;
+ }
+ const draft = this._editDraft;
+ const postId = this._expandedPostId;
+ const hashtags = draft.hashtags
+ .split(/[\s,]+/)
+ .map(s => s.trim())
+ .filter(Boolean)
+ .map(s => s.startsWith('#') ? s : `#${s}`);
+ // datetime-local string → ISO
+ const scheduledAtIso = draft.scheduledAt ? new Date(draft.scheduledAt).toISOString() : '';
+
+ this._savingIndicator = 'Saving…';
+ this.render();
+
+ try {
+ const docId = socialsDocId(this._space) as DocumentId;
+ await runtime.changeDoc(docId, 'edit post', (d: SocialsDoc) => {
+ if (!d.campaigns) return;
+ for (const campaign of Object.values(d.campaigns)) {
+ const post = (campaign.posts || []).find((p: CampaignPost) => p.id === postId);
+ if (post) {
+ post.content = draft.content;
+ post.status = draft.status;
+ post.scheduledAt = scheduledAtIso;
+ post.hashtags = hashtags;
+ return;
+ }
+ }
+ });
+ this._savingIndicator = 'Saved';
+ this._editMode = false;
+ this._editDraft = null;
+ this.render();
+ setTimeout(() => { this._savingIndicator = ''; this.render(); }, 1500);
+ } catch (err) {
+ console.warn('[folk-thread-gallery] save failed', err);
+ this._savingIndicator = 'Save failed';
+ this.render();
+ }
+ }
+}
+
+function toDatetimeLocal(iso: string): string {
+ try {
+ const d = new Date(iso);
+ if (isNaN(d.getTime())) return '';
+ const pad = (n: number) => String(n).padStart(2, '0');
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
+ } catch {
+ return '';
}
}
diff --git a/modules/rsocials/mod.ts b/modules/rsocials/mod.ts
index b7f378ba..87f9d967 100644
--- a/modules/rsocials/mod.ts
+++ b/modules/rsocials/mod.ts
@@ -3026,7 +3026,7 @@ routes.get("/threads", (c) => {
theme: "dark",
body: ` `,
styles: ` `,
- scripts: ``,
+ scripts: ``,
}));
});
@@ -3181,7 +3181,7 @@ routes.get("/", (c) => {
theme: "dark",
styles: ` `,
body: ` `,
- scripts: ``,
+ scripts: ``,
}));
});