diff --git a/modules/rsocials/components/campaign-planner.css b/modules/rsocials/components/campaign-planner.css
index 75245b07..720b77ed 100644
--- a/modules/rsocials/components/campaign-planner.css
+++ b/modules/rsocials/components/campaign-planner.css
@@ -160,69 +160,80 @@ folk-campaign-planner {
.cp-pp-list {
flex: 1;
overflow-y: auto;
- padding: 8px;
- display: flex;
- flex-direction: column;
- gap: 5px;
+ padding: 10px;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+ align-content: start;
}
.cp-pp-chip {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 8px 10px;
- border-radius: 8px;
- background: var(--rs-bg-surface, #1a1a2e);
- border: 1px solid var(--rs-border, #2d2d44);
+ position: relative;
cursor: grab;
user-select: none;
- transition: border-color 0.1s, background 0.1s, transform 0.05s;
-}
-
-.cp-pp-chip:hover {
- border-color: var(--rs-border-strong, #3d3d5c);
- background: var(--rs-bg-surface-raised, #252540);
+ padding: 0;
+ background: transparent;
+ border: none;
}
.cp-pp-chip:active { cursor: grabbing; }
-.cp-pp-chip.dragging {
- opacity: 0.5;
- transform: scale(0.96);
+.cp-pp-chip.dragging { opacity: 0.4; }
+
+.cp-pp-chip__card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 6px;
+ padding: 12px 6px 10px;
+ border-radius: 10px;
+ background: linear-gradient(180deg, var(--plat-bg, rgba(59,130,246,0.12)), transparent 60%), var(--rs-bg-surface, #1a1a2e);
+ border: 1.5px solid var(--plat-ring, rgba(255,255,255,0.08));
+ box-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 3px 8px rgba(0,0,0,0.25);
+ transition: transform 0.1s ease, box-shadow 0.1s ease, border-color 0.1s ease;
+}
+
+.cp-pp-chip:hover .cp-pp-chip__card {
+ transform: translateY(-1px);
+ border-color: var(--plat, #3b82f6);
+ box-shadow: 0 1px 0 rgba(255,255,255,0.06), 0 6px 14px rgba(0,0,0,0.35);
}
.cp-pp-chip__icon {
- width: 28px;
- height: 28px;
- border-radius: 6px;
+ width: 38px;
+ height: 38px;
+ border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
- font-weight: 700;
- font-size: 14px;
- flex-shrink: 0;
-}
-
-.cp-pp-chip__meta {
- min-width: 0;
- flex: 1;
+ font-weight: 800;
+ font-size: 20px;
+ color: var(--plat, #3b82f6);
+ background: var(--plat-bg, rgba(59,130,246,0.15));
+ border: 1px solid var(--plat-ring, rgba(59,130,246,0.35));
}
.cp-pp-chip__label {
- font-size: 12px;
+ font-size: 11.5px;
font-weight: 600;
color: var(--rs-text-primary, #e1e1e1);
+ letter-spacing: 0.1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ max-width: 100%;
+ text-align: center;
}
.cp-pp-chip__limit {
- font-size: 10px;
+ font-size: 9.5px;
color: var(--rs-text-muted, #888);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ max-width: 100%;
+ text-align: center;
+ line-height: 1.25;
}
.cp-canvas--drop-target {
@@ -336,10 +347,293 @@ folk-campaign-planner {
}
@media (max-width: 720px) {
- .cp-pp { width: 56px; }
- .cp-pp-title, .cp-pp-hint { display: none; }
- .cp-pp-chip__meta { display: none; }
- .cp-pp-chip { padding: 6px; justify-content: center; }
+ .cp-pp { width: 84px; }
+ .cp-pp-hint { display: none; }
+ .cp-pp-list { grid-template-columns: 1fr; }
+ .cp-pp-chip__label { font-size: 10.5px; }
+ .cp-pp-chip__limit { display: none; }
+}
+
+/* ── Board view (Drafts / Scheduled / Published) ── */
+.cp-board-view {
+ position: relative;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ background: var(--rs-bg-surface-sunken, #14141e);
+ color: var(--rs-text-primary, #e1e1e1);
+}
+
+.cp-bv-switcher {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 10px 16px;
+ border-bottom: 1px solid var(--rs-border, #2d2d44);
+ background: var(--rs-bg-surface, #1a1a2e);
+ font-size: 12px;
+ flex-shrink: 0;
+}
+
+.cp-bv-switcher__label { color: var(--rs-text-muted, #888); font-weight: 600; }
+.cp-bv-switcher__item {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ cursor: pointer;
+ color: var(--rs-text-secondary, #a0a0b8);
+}
+.cp-bv-switcher__item input { accent-color: #3b82f6; }
+
+.cp-bv-cols {
+ flex: 1;
+ display: grid;
+ grid-auto-columns: minmax(240px, 1fr);
+ grid-auto-flow: column;
+ gap: 12px;
+ padding: 14px;
+ overflow: auto;
+}
+
+.cp-bv-col {
+ display: flex;
+ flex-direction: column;
+ min-width: 240px;
+ background: var(--rs-bg-surface, #1a1a2e);
+ border: 1px solid var(--rs-border, #2d2d44);
+ border-radius: 10px;
+ overflow: hidden;
+}
+
+.cp-bv-col--collapsed {
+ min-width: 42px;
+ max-width: 42px;
+}
+.cp-bv-col--collapsed .cp-bv-col__title,
+.cp-bv-col--collapsed .cp-bv-col__count,
+.cp-bv-col--collapsed .cp-bv-col__drop { display: none; }
+.cp-bv-col--collapsed .cp-bv-col__head {
+ flex-direction: column;
+ gap: 8px;
+ padding: 10px 6px;
+}
+
+.cp-bv-col__head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 12px;
+ border-bottom: 1px solid var(--rs-border, #2d2d44);
+ background: var(--rs-bg-surface-raised, #252540);
+}
+
+.cp-bv-col__toggle {
+ width: 22px; height: 22px;
+ border-radius: 6px;
+ border: 1px solid var(--rs-border, #3d3d5c);
+ background: transparent;
+ color: var(--rs-text-secondary, #a0a0b8);
+ font-size: 14px;
+ font-weight: 700;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.cp-bv-col__toggle:hover { background: var(--rs-bg-surface, #1a1a2e); }
+
+.cp-bv-col__title {
+ font-size: 12.5px;
+ font-weight: 700;
+ color: var(--rs-text-primary, #e1e1e1);
+ flex: 1;
+ letter-spacing: 0.2px;
+}
+
+.cp-bv-col__count {
+ font-size: 11px;
+ padding: 1px 7px;
+ border-radius: 999px;
+ background: rgba(255,255,255,0.05);
+ color: var(--rs-text-muted, #888);
+ font-weight: 600;
+}
+
+.cp-bv-col__drop {
+ flex: 1;
+ padding: 10px;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-height: 80px;
+ transition: background 0.1s ease;
+}
+
+.cp-bv-drop-hover {
+ background: rgba(59, 130, 246, 0.08);
+ outline: 2px dashed #3b82f6;
+ outline-offset: -6px;
+}
+
+.cp-bv-empty {
+ margin: auto;
+ text-align: center;
+ color: var(--rs-text-muted, #888);
+ padding: 30px 12px;
+ font-size: 12px;
+}
+.cp-bv-empty strong { display: block; font-size: 13px; color: var(--rs-text-secondary, #a0a0b8); margin-bottom: 4px; }
+.cp-bv-empty p { margin: 0; line-height: 1.4; }
+
+.cp-bv-card {
+ background: var(--rs-bg-surface-raised, #252540);
+ border: 1px solid var(--rs-border, #2d2d44);
+ border-left: 3px solid var(--plat, #3b82f6);
+ border-radius: 8px;
+ padding: 10px 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ cursor: grab;
+ user-select: none;
+ transition: border-color 0.1s ease, transform 0.05s ease;
+}
+.cp-bv-card:hover { border-color: var(--plat, #3b82f6); }
+.cp-bv-card.dragging { opacity: 0.4; }
+.cp-bv-card:active { cursor: grabbing; }
+
+.cp-bv-card__head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.cp-bv-card__icon {
+ width: 24px; height: 24px;
+ border-radius: 6px;
+ display: flex; align-items: center; justify-content: center;
+ font-weight: 700; font-size: 13px;
+ flex-shrink: 0;
+}
+
+.cp-bv-card__plat {
+ flex: 1;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--rs-text-primary, #e1e1e1);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.cp-bv-err, .cp-bv-queued {
+ font-size: 10px;
+ padding: 1px 6px;
+ border-radius: 999px;
+ font-weight: 600;
+}
+.cp-bv-err { background: rgba(239,68,68,0.15); color: #ef4444; border: 1px solid rgba(239,68,68,0.35); }
+.cp-bv-queued { background: rgba(168,85,247,0.15); color: #c084fc; border: 1px solid rgba(168,85,247,0.35); }
+
+.cp-bv-card__body {
+ font-size: 12px;
+ color: var(--rs-text-secondary, #cbd5e1);
+ line-height: 1.4;
+ max-height: 3.9em;
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+}
+
+.cp-bv-card__foot {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 6px;
+ font-size: 10px;
+ color: var(--rs-text-muted, #888);
+}
+
+.cp-bv-card__bar {
+ flex: 1;
+ min-width: 40px;
+ height: 3px;
+ background: rgba(255,255,255,0.06);
+ border-radius: 2px;
+ overflow: hidden;
+}
+.cp-bv-card__bar > div { height: 100%; border-radius: 2px; }
+
+.cp-bv-card__chars.over { color: #ef4444; font-weight: 700; }
+
+.cp-bv-card__when {
+ background: rgba(255,255,255,0.04);
+ padding: 1px 6px;
+ border-radius: 999px;
+ white-space: nowrap;
+}
+
+.cp-bv-card__edit {
+ margin-left: auto;
+ padding: 2px 8px;
+ border: 1px solid var(--rs-border, #3d3d5c);
+ background: transparent;
+ color: var(--rs-text-secondary, #a0a0b8);
+ border-radius: 6px;
+ font-size: 10px;
+ cursor: pointer;
+}
+.cp-bv-card__edit:hover { background: var(--rs-bg-surface, #1a1a2e); color: var(--rs-text-primary, #e1e1e1); }
+
+.cp-bv-link {
+ color: #3b82f6;
+ text-decoration: none;
+}
+.cp-bv-link:hover { text-decoration: underline; }
+
+/* Scheduled-at picker modal */
+.cp-bv-sched-modal[hidden] { display: none; }
+.cp-bv-sched-modal {
+ position: absolute;
+ inset: 0;
+ z-index: 30;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.cp-bv-sched-modal__backdrop {
+ position: absolute;
+ inset: 0;
+ background: rgba(0,0,0,0.45);
+}
+.cp-bv-sched-modal__panel {
+ position: relative;
+ width: min(380px, calc(100% - 32px));
+ background: var(--rs-bg-surface, #1a1a2e);
+ border: 1px solid var(--rs-border-strong, #3d3d5c);
+ border-radius: 12px;
+ padding: 18px 18px 14px;
+ box-shadow: 0 16px 40px rgba(0,0,0,0.5);
+}
+.cp-bv-sched-modal h3 { margin: 0 0 4px; font-size: 15px; }
+.cp-bv-sched-modal__hint { margin: 0 0 14px; font-size: 11px; color: var(--rs-text-muted, #888); }
+.cp-bv-sched-modal label { display: block; font-size: 11px; color: var(--rs-text-muted, #888); margin-bottom: 6px; }
+.cp-bv-sched-modal input {
+ width: 100%;
+ padding: 8px 10px;
+ border-radius: 6px;
+ border: 1px solid var(--rs-border-strong, #3d3d5c);
+ background: var(--rs-input-bg, #16162a);
+ color: var(--rs-text-primary, #e1e1e1);
+ font-size: 13px;
+}
+.cp-bv-sched-modal__actions {
+ display: flex;
+ gap: 8px;
+ justify-content: flex-end;
+ margin-top: 14px;
}
.cp-canvas {
@@ -526,8 +820,10 @@ folk-campaign-planner {
display: flex;
flex-direction: column;
gap: 8px;
- max-height: 300px;
+ max-height: 380px;
overflow-y: auto;
+ overscroll-behavior: contain;
+ touch-action: pan-y;
}
.cp-icp-body label {
diff --git a/modules/rsocials/components/folk-campaign-planner.ts b/modules/rsocials/components/folk-campaign-planner.ts
index 43f1657a..77bd2f6a 100644
--- a/modules/rsocials/components/folk-campaign-planner.ts
+++ b/modules/rsocials/components/folk-campaign-planner.ts
@@ -154,7 +154,7 @@ function getUsername(): string | null {
class FolkCampaignPlanner extends HTMLElement {
private shadow: ShadowRoot;
private space = '';
- private currentView: 'canvas' | 'timeline' | 'platform' | 'table' = 'canvas';
+ private currentView: 'canvas' | 'timeline' | 'platform' | 'table' | 'board' = 'canvas';
private get basePath() {
const host = window.location.hostname;
@@ -825,8 +825,8 @@ class FolkCampaignPlanner extends HTMLElement {
const overlay = document.createElementNS('http://www.w3.org/2000/svg', 'g');
overlay.classList.add('inline-edit-overlay');
- const panelW = 260;
- const panelH = node.type === 'post' ? 380 : node.type === 'thread' ? 240 : node.type === 'goal' ? 260 : node.type === 'tone' ? 300 : node.type === 'brief' ? 400 : 220;
+ const panelW = node.type === 'post' ? 300 : 260;
+ const panelH = node.type === 'post' ? 460 : node.type === 'thread' ? 240 : node.type === 'goal' ? 260 : node.type === 'tone' ? 300 : node.type === 'brief' ? 400 : 220;
const panelX = s.w + 12;
const panelY = 0;
@@ -1437,7 +1437,7 @@ class FolkCampaignPlanner extends HTMLElement {
// ── View switching ──
- private switchView(view: 'canvas' | 'timeline' | 'platform' | 'table') {
+ private switchView(view: 'canvas' | 'timeline' | 'platform' | 'table' | 'board') {
this.currentView = view;
const canvasEl = this.shadow.getElementById('cp-canvas');
const altEl = this.shadow.getElementById('cp-alt-view');
@@ -1465,8 +1465,10 @@ class FolkCampaignPlanner extends HTMLElement {
case 'timeline': altEl.innerHTML = this.renderTimeline(); break;
case 'platform': altEl.innerHTML = this.renderPlatformView(); break;
case 'table': altEl.innerHTML = this.renderTableView(); break;
+ case 'board': altEl.innerHTML = this.renderBoardView(); break;
}
this.attachAltViewListeners();
+ if (view === 'board') this.attachBoardListeners();
}
// ── Timeline view ──
@@ -1720,6 +1722,284 @@ class FolkCampaignPlanner extends HTMLElement {
`;
}
+ // ── Board view (Drafts / Scheduled / Published) ──
+
+ /** Which columns are currently visible. At least one must stay on. */
+ private boardVisible: { drafts: boolean; scheduled: boolean; published: boolean } = {
+ drafts: true, scheduled: true, published: true,
+ };
+
+ private renderBoardView(): string {
+ const posts = this.nodes.filter(n => n.type === 'post');
+ const drafts = posts.filter(n => {
+ const d = n.data as PostNodeData;
+ return d.status === 'draft' && !d.postizPostId;
+ });
+ const scheduled = posts.filter(n => {
+ const d = n.data as PostNodeData;
+ return (d.status === 'scheduled' || d.postizStatus === 'queued') && d.postizStatus !== 'published';
+ });
+ const published = posts.filter(n => {
+ const d = n.data as PostNodeData;
+ return d.status === 'published' || d.postizStatus === 'published';
+ });
+
+ const renderCard = (node: CampaignPlannerNode) => {
+ const d = node.data as PostNodeData;
+ const platform = d.platform || 'x';
+ const spec = getPlatformSpec(platform);
+ const color = spec?.color || PLATFORM_COLORS[platform] || '#888';
+ const icon = spec?.icon || PLATFORM_ICONS[platform] || platform.charAt(0);
+ const label = spec?.label || platform;
+ const charCount = (d.content || '').length;
+ const charMax = charLimitFor(platform);
+ const charPct = Math.min(1, charCount / charMax) * 100;
+ const overLimit = charCount > charMax;
+ const preview = (d.content || '').split('\n')[0].substring(0, 80);
+ const scheduledStr = d.scheduledAt
+ ? new Date(d.scheduledAt).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })
+ : '';
+ const publishedStr = d.publishedAt
+ ? new Date(d.publishedAt).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })
+ : '';
+ const errorBadge = d.postizStatus === 'failed'
+ ? `⚠ error` : '';
+ const queuedBadge = d.postizStatus === 'queued'
+ ? '⏳ Postiz' : '';
+ const releaseLink = d.postizReleaseURL
+ ? `Open ↗` : '';
+
+ return `
${hint}
Pick a publish date & time. Postiz will pick it up on the next sweep.
+ + +