Merge branch 'dev'
This commit is contained in:
commit
4ccd23640a
|
|
@ -174,6 +174,12 @@ export class FolkFeed extends FolkShape {
|
||||||
this.#feedData = data.nodes.slice(0, this.maxItems);
|
this.#feedData = data.nodes.slice(0, this.maxItems);
|
||||||
} else if (data.flows) {
|
} else if (data.flows) {
|
||||||
this.#feedData = data.flows.slice(0, this.maxItems);
|
this.#feedData = data.flows.slice(0, this.maxItems);
|
||||||
|
} else if (data.threads) {
|
||||||
|
this.#feedData = data.threads.slice(0, this.maxItems);
|
||||||
|
} else if (data.campaigns) {
|
||||||
|
this.#feedData = data.campaigns.slice(0, this.maxItems);
|
||||||
|
} else if (data.drafts) {
|
||||||
|
this.#feedData = data.drafts.slice(0, this.maxItems);
|
||||||
} else {
|
} else {
|
||||||
// Try to use the data as-is if it has array-like fields
|
// Try to use the data as-is if it has array-like fields
|
||||||
const firstArray = Object.values(data).find(v => Array.isArray(v));
|
const firstArray = Object.values(data).find(v => Array.isArray(v));
|
||||||
|
|
@ -238,6 +244,12 @@ export class FolkFeed extends FolkShape {
|
||||||
itinerary: "trips",
|
itinerary: "trips",
|
||||||
default: "trips",
|
default: "trips",
|
||||||
},
|
},
|
||||||
|
socials: {
|
||||||
|
threads: "threads",
|
||||||
|
campaigns: "campaigns",
|
||||||
|
newsletter: "newsletter/drafts",
|
||||||
|
default: "threads",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const moduleEndpoints = FEED_ENDPOINTS[this.sourceModule];
|
const moduleEndpoints = FEED_ENDPOINTS[this.sourceModule];
|
||||||
|
|
|
||||||
|
|
@ -451,6 +451,10 @@ const MODULE_PORTS: Record<string, PortDescriptor[]> = {
|
||||||
rwallet: [{ name: "balance-out", type: "number", direction: "output" },
|
rwallet: [{ name: "balance-out", type: "number", direction: "output" },
|
||||||
{ name: "transfer-trigger", type: "trigger", direction: "input" },
|
{ name: "transfer-trigger", type: "trigger", direction: "input" },
|
||||||
{ name: "transfer-data", type: "json", direction: "input" }],
|
{ name: "transfer-data", type: "json", direction: "input" }],
|
||||||
|
rsocials: [{ name: "threads-out", type: "json", direction: "output" },
|
||||||
|
{ name: "campaigns-out", type: "json", direction: "output" },
|
||||||
|
{ name: "post-published", type: "trigger", direction: "output" },
|
||||||
|
{ name: "campaign-data", type: "json", direction: "output" }],
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PORTS: PortDescriptor[] = [
|
const DEFAULT_PORTS: PortDescriptor[] = [
|
||||||
|
|
@ -552,6 +556,20 @@ const WIDGET_API: Record<string, { path: string; transform: (data: any) => Widge
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
rsocials: {
|
||||||
|
path: "/api/threads",
|
||||||
|
transform: (data) => {
|
||||||
|
const threads = data?.threads || [];
|
||||||
|
const campaigns = data?.campaignCount ?? 0;
|
||||||
|
return {
|
||||||
|
stat: `${threads.length} thread${threads.length !== 1 ? "s" : ""}`,
|
||||||
|
rows: threads.slice(0, 3).map((t: any) => ({
|
||||||
|
label: t.title || "Untitled",
|
||||||
|
value: `${t.tweets?.length || 0} tweets`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
rnetwork: {
|
rnetwork: {
|
||||||
path: "/api/graph",
|
path: "/api/graph",
|
||||||
transform: (data) => {
|
transform: (data) => {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ const TOOL_HINTS: ToolHint[] = [
|
||||||
{ tagName: "folk-rapp", label: "rMeets", icon: "📹", keywords: ["meeting", "jitsi", "video", "meet", "conference", "rmeets"] },
|
{ tagName: "folk-rapp", label: "rMeets", icon: "📹", keywords: ["meeting", "jitsi", "video", "meet", "conference", "rmeets"] },
|
||||||
{ tagName: "folk-workflow-block", label: "Workflow", icon: "⚙️", keywords: ["workflow", "automation", "block", "process"] },
|
{ tagName: "folk-workflow-block", label: "Workflow", icon: "⚙️", keywords: ["workflow", "automation", "block", "process"] },
|
||||||
{ tagName: "folk-social-post", label: "Social Post", icon: "📣", keywords: ["social", "post", "twitter", "instagram", "campaign"] },
|
{ tagName: "folk-social-post", label: "Social Post", icon: "📣", keywords: ["social", "post", "twitter", "instagram", "campaign"] },
|
||||||
|
{ tagName: "folk-social-thread", label: "Thread", icon: "🧵", keywords: ["thread", "tweetstorm", "twitter thread", "tweets", "multi-post"] },
|
||||||
|
{ tagName: "folk-social-campaign", label: "Campaign", icon: "📢", keywords: ["campaign", "launch", "marketing", "social campaign", "content plan"] },
|
||||||
|
{ tagName: "folk-social-newsletter", label: "Newsletter", icon: "📧", keywords: ["newsletter", "email", "mailout", "subscriber", "mailing list"] },
|
||||||
{ tagName: "folk-splat", label: "3D Gaussian", icon: "💎", keywords: ["3d", "splat", "gaussian", "point cloud"] },
|
{ tagName: "folk-splat", label: "3D Gaussian", icon: "💎", keywords: ["3d", "splat", "gaussian", "point cloud"] },
|
||||||
{ tagName: "folk-drawfast", label: "Drawing", icon: "✏️", keywords: ["draw", "sketch", "whiteboard", "pencil"] },
|
{ tagName: "folk-drawfast", label: "Drawing", icon: "✏️", keywords: ["draw", "sketch", "whiteboard", "pencil"] },
|
||||||
{ tagName: "folk-rapp", label: "rApp Embed", icon: "📦", keywords: ["rapp", "module", "embed", "app", "crm", "contacts", "pipeline", "companies"] },
|
{ tagName: "folk-rapp", label: "rApp Embed", icon: "📦", keywords: ["rapp", "module", "embed", "app", "crm", "contacts", "pipeline", "companies"] },
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ const SHAPE_ICONS: Record<string, { icon: string; label: string }> = {
|
||||||
"folk-map": { icon: "🗺️", label: "Map" },
|
"folk-map": { icon: "🗺️", label: "Map" },
|
||||||
"folk-workflow-block": { icon: "⚙️", label: "Workflow" },
|
"folk-workflow-block": { icon: "⚙️", label: "Workflow" },
|
||||||
"folk-social-post": { icon: "📣", label: "Social Post" },
|
"folk-social-post": { icon: "📣", label: "Social Post" },
|
||||||
|
"folk-social-thread": { icon: "🧵", label: "Thread" },
|
||||||
|
"folk-social-campaign": { icon: "📢", label: "Campaign" },
|
||||||
|
"folk-social-newsletter": { icon: "📧", label: "Newsletter" },
|
||||||
"folk-choice-vote": { icon: "🗳️", label: "Vote" },
|
"folk-choice-vote": { icon: "🗳️", label: "Vote" },
|
||||||
"folk-prompt": { icon: "🤖", label: "AI Chat" },
|
"folk-prompt": { icon: "🤖", label: "AI Chat" },
|
||||||
"folk-image-gen": { icon: "🎨", label: "AI Image" },
|
"folk-image-gen": { icon: "🎨", label: "AI Image" },
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ import { ySyncPlugin, yUndoPlugin, yCursorPlugin } from '@tiptap/y-tiptap';
|
||||||
import { RSpaceYjsProvider } from '../yjs-ws-provider';
|
import { RSpaceYjsProvider } from '../yjs-ws-provider';
|
||||||
import { CommentMark } from './comment-mark';
|
import { CommentMark } from './comment-mark';
|
||||||
import { SuggestionInsertMark, SuggestionDeleteMark } from './suggestion-marks';
|
import { SuggestionInsertMark, SuggestionDeleteMark } from './suggestion-marks';
|
||||||
import { createSuggestionPlugin, acceptSuggestion, rejectSuggestion, acceptAllSuggestions, rejectAllSuggestions } from './suggestion-plugin';
|
import { createSuggestionPlugin, acceptSuggestion, rejectSuggestion } from './suggestion-plugin';
|
||||||
import './comment-panel';
|
import './comment-panel';
|
||||||
|
|
||||||
const lowlight = createLowlight(common);
|
const lowlight = createLowlight(common);
|
||||||
|
|
@ -295,7 +295,16 @@ class FolkNotesApp extends HTMLElement {
|
||||||
rightCol.appendChild(this.contentZone);
|
rightCol.appendChild(this.contentZone);
|
||||||
rightCol.appendChild(this.metaZone);
|
rightCol.appendChild(this.metaZone);
|
||||||
|
|
||||||
|
// Sidebar reopen tab (lives on layout, outside navZone so it's visible when collapsed)
|
||||||
|
const reopenBtn = document.createElement('button');
|
||||||
|
reopenBtn.id = 'sidebar-reopen';
|
||||||
|
reopenBtn.className = 'sidebar-reopen';
|
||||||
|
reopenBtn.title = 'Show sidebar';
|
||||||
|
reopenBtn.textContent = '\u203A';
|
||||||
|
reopenBtn.addEventListener('click', () => this.toggleSidebar(true));
|
||||||
|
|
||||||
layout.appendChild(this.navZone);
|
layout.appendChild(this.navZone);
|
||||||
|
layout.appendChild(reopenBtn);
|
||||||
layout.appendChild(rightCol);
|
layout.appendChild(rightCol);
|
||||||
|
|
||||||
this.shadow.appendChild(style);
|
this.shadow.appendChild(style);
|
||||||
|
|
@ -2372,25 +2381,12 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
bar.innerHTML = `
|
bar.innerHTML = `
|
||||||
<span class="srb-label">${this.suggestingMode ? 'Suggesting' : 'Editing'}</span>
|
<span class="srb-label">${this.suggestingMode ? 'Suggesting' : 'Editing'}</span>
|
||||||
${ids.size > 0 ? `
|
${ids.size > 0 ? `
|
||||||
<span class="srb-count">${ids.size} suggestion${ids.size !== 1 ? 's' : ''}</span>
|
<span class="srb-count">${ids.size} suggestion${ids.size !== 1 ? 's' : ''} — review in sidebar</span>
|
||||||
<button class="srb-btn srb-accept-all" data-action="accept-all-suggestions" title="Accept all suggestions">Accept All</button>
|
|
||||||
<button class="srb-btn srb-reject-all" data-action="reject-all-suggestions" title="Reject all suggestions">Reject All</button>
|
|
||||||
` : '<span class="srb-hint">Start typing to suggest changes</span>'}
|
` : '<span class="srb-hint">Start typing to suggest changes</span>'}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Wire buttons
|
// Open sidebar to show suggestions when there are any
|
||||||
bar.querySelector('[data-action="accept-all-suggestions"]')?.addEventListener('click', () => {
|
if (ids.size > 0) this.showCommentPanel();
|
||||||
if (this.editor) {
|
|
||||||
acceptAllSuggestions(this.editor);
|
|
||||||
this.updateSuggestionReviewBar();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
bar.querySelector('[data-action="reject-all-suggestions"]')?.addEventListener('click', () => {
|
|
||||||
if (this.editor) {
|
|
||||||
rejectAllSuggestions(this.editor);
|
|
||||||
this.updateSuggestionReviewBar();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Show an accept/reject popover near a clicked suggestion mark. */
|
/** Show an accept/reject popover near a clicked suggestion mark. */
|
||||||
|
|
@ -2420,6 +2416,7 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
if (this.editor) {
|
if (this.editor) {
|
||||||
acceptSuggestion(this.editor, suggestionId);
|
acceptSuggestion(this.editor, suggestionId);
|
||||||
this.updateSuggestionReviewBar();
|
this.updateSuggestionReviewBar();
|
||||||
|
this.syncSuggestionsToPanel();
|
||||||
}
|
}
|
||||||
pop.remove();
|
pop.remove();
|
||||||
});
|
});
|
||||||
|
|
@ -2427,6 +2424,7 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
if (this.editor) {
|
if (this.editor) {
|
||||||
rejectSuggestion(this.editor, suggestionId);
|
rejectSuggestion(this.editor, suggestionId);
|
||||||
this.updateSuggestionReviewBar();
|
this.updateSuggestionReviewBar();
|
||||||
|
this.syncSuggestionsToPanel();
|
||||||
}
|
}
|
||||||
pop.remove();
|
pop.remove();
|
||||||
});
|
});
|
||||||
|
|
@ -2443,6 +2441,47 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
setTimeout(() => this.shadow.addEventListener('click', close), 0);
|
setTimeout(() => this.shadow.addEventListener('click', close), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Collect all pending suggestions from the editor doc. */
|
||||||
|
private collectSuggestions(): { id: string; type: 'insert' | 'delete'; text: string; authorId: string; authorName: string; createdAt: number }[] {
|
||||||
|
if (!this.editor) return [];
|
||||||
|
const map = new Map<string, { id: string; type: 'insert' | 'delete'; text: string; authorId: string; authorName: string; createdAt: number }>();
|
||||||
|
this.editor.state.doc.descendants((node: any) => {
|
||||||
|
if (!node.isText) return;
|
||||||
|
for (const mark of node.marks) {
|
||||||
|
if (mark.type.name === 'suggestionInsert' || mark.type.name === 'suggestionDelete') {
|
||||||
|
const id = mark.attrs.suggestionId;
|
||||||
|
const existing = map.get(id);
|
||||||
|
if (existing) {
|
||||||
|
existing.text += node.text || '';
|
||||||
|
} else {
|
||||||
|
map.set(id, {
|
||||||
|
id,
|
||||||
|
type: mark.type.name === 'suggestionInsert' ? 'insert' : 'delete',
|
||||||
|
text: node.text || '',
|
||||||
|
authorId: mark.attrs.authorId || '',
|
||||||
|
authorName: mark.attrs.authorName || 'Unknown',
|
||||||
|
createdAt: mark.attrs.createdAt || Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Array.from(map.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Push current suggestions to the comment panel and ensure sidebar is visible. */
|
||||||
|
private syncSuggestionsToPanel() {
|
||||||
|
const panel = this.shadow.querySelector('notes-comment-panel') as any;
|
||||||
|
if (!panel) return;
|
||||||
|
const suggestions = this.collectSuggestions();
|
||||||
|
panel.suggestions = suggestions;
|
||||||
|
// Show sidebar if there are suggestions or comments
|
||||||
|
const sidebar = this.shadow.getElementById('comment-sidebar');
|
||||||
|
if (sidebar && suggestions.length > 0) {
|
||||||
|
sidebar.classList.add('has-comments');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Show comment panel for a specific thread. */
|
/** Show comment panel for a specific thread. */
|
||||||
private showCommentPanel(threadId?: string) {
|
private showCommentPanel(threadId?: string) {
|
||||||
const sidebar = this.shadow.getElementById('comment-sidebar');
|
const sidebar = this.shadow.getElementById('comment-sidebar');
|
||||||
|
|
@ -2457,6 +2496,21 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
const { noteId, threads } = e.detail;
|
const { noteId, threads } = e.detail;
|
||||||
if (noteId) this._demoThreads.set(noteId, threads);
|
if (noteId) this._demoThreads.set(noteId, threads);
|
||||||
});
|
});
|
||||||
|
// Listen for suggestion accept/reject from comment panel
|
||||||
|
panel.addEventListener('suggestion-accept', (e: CustomEvent) => {
|
||||||
|
if (this.editor && e.detail?.suggestionId) {
|
||||||
|
acceptSuggestion(this.editor, e.detail.suggestionId);
|
||||||
|
this.updateSuggestionReviewBar();
|
||||||
|
this.syncSuggestionsToPanel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
panel.addEventListener('suggestion-reject', (e: CustomEvent) => {
|
||||||
|
if (this.editor && e.detail?.suggestionId) {
|
||||||
|
rejectSuggestion(this.editor, e.detail.suggestionId);
|
||||||
|
this.updateSuggestionReviewBar();
|
||||||
|
this.syncSuggestionsToPanel();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
panel.noteId = this.editorNoteId;
|
panel.noteId = this.editorNoteId;
|
||||||
panel.doc = this.doc;
|
panel.doc = this.doc;
|
||||||
|
|
@ -2470,8 +2524,10 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
} else {
|
} else {
|
||||||
panel.demoThreads = null;
|
panel.demoThreads = null;
|
||||||
}
|
}
|
||||||
|
// Pass suggestions
|
||||||
|
panel.suggestions = this.collectSuggestions();
|
||||||
|
|
||||||
// Show sidebar when there are comments
|
// Show sidebar when there are comments or suggestions
|
||||||
sidebar.classList.add('has-comments');
|
sidebar.classList.add('has-comments');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2496,9 +2552,10 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// On any change, update the suggestion review bar
|
// On any change, update the suggestion review bar + sidebar panel
|
||||||
this.editor.on('update', () => {
|
this.editor.on('update', () => {
|
||||||
this.updateSuggestionReviewBar();
|
this.updateSuggestionReviewBar();
|
||||||
|
this.syncSuggestionsToPanel();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Direct click on comment highlight or suggestion marks in the DOM
|
// Direct click on comment highlight or suggestion marks in the DOM
|
||||||
|
|
@ -2802,6 +2859,7 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
<div class="notes-sidebar">
|
<div class="notes-sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<input class="sidebar-search" type="text" placeholder="Search notes..." id="search-input" value="${this.esc(this.searchQuery)}">
|
<input class="sidebar-search" type="text" placeholder="Search notes..." id="search-input" value="${this.esc(this.searchQuery)}">
|
||||||
|
<button class="sidebar-collapse" id="sidebar-collapse" title="Hide sidebar">\u2039</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="sidebar-btn-new-nb" id="create-notebook">+ New Notebook</button>
|
<button class="sidebar-btn-new-nb" id="create-notebook">+ New Notebook</button>
|
||||||
<div class="sidebar-tree">
|
<div class="sidebar-tree">
|
||||||
|
|
@ -2818,6 +2876,10 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Apply collapsed state
|
||||||
|
const layout = this.shadow.getElementById('notes-layout');
|
||||||
|
if (layout) layout.classList.toggle('sidebar-collapsed', !this.sidebarOpen);
|
||||||
|
|
||||||
// Restore search focus
|
// Restore search focus
|
||||||
if (hadFocus) {
|
if (hadFocus) {
|
||||||
const newInput = this.navZone.querySelector('#search-input') as HTMLInputElement;
|
const newInput = this.navZone.querySelector('#search-input') as HTMLInputElement;
|
||||||
|
|
@ -2934,9 +2996,18 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private toggleSidebar(open?: boolean) {
|
||||||
|
this.sidebarOpen = open !== undefined ? open : !this.sidebarOpen;
|
||||||
|
const layout = this.shadow.getElementById('notes-layout');
|
||||||
|
if (layout) layout.classList.toggle('sidebar-collapsed', !this.sidebarOpen);
|
||||||
|
}
|
||||||
|
|
||||||
private attachSidebarListeners() {
|
private attachSidebarListeners() {
|
||||||
const isDemo = this.space === "demo";
|
const isDemo = this.space === "demo";
|
||||||
|
|
||||||
|
// Sidebar collapse (reopen button is wired once in connectedCallback)
|
||||||
|
this.shadow.getElementById('sidebar-collapse')?.addEventListener('click', () => this.toggleSidebar(false));
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
const searchInput = this.shadow.getElementById("search-input") as HTMLInputElement;
|
const searchInput = this.shadow.getElementById("search-input") as HTMLInputElement;
|
||||||
let searchTimeout: any;
|
let searchTimeout: any;
|
||||||
|
|
@ -3091,7 +3162,21 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
grid-template-columns: 260px 1fr;
|
grid-template-columns: 260px 1fr;
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
height: calc(100vh - 120px);
|
height: calc(100vh - 120px);
|
||||||
|
position: relative;
|
||||||
|
transition: grid-template-columns 0.2s ease;
|
||||||
}
|
}
|
||||||
|
#notes-layout.sidebar-collapsed {
|
||||||
|
grid-template-columns: 0px 1fr;
|
||||||
|
}
|
||||||
|
#notes-layout.sidebar-collapsed .notes-sidebar {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
#notes-layout.sidebar-collapsed .sidebar-reopen {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
#nav-zone { overflow: hidden; }
|
||||||
.notes-sidebar {
|
.notes-sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -3099,10 +3184,45 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
background: var(--rs-bg-surface);
|
background: var(--rs-bg-surface);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
}
|
}
|
||||||
.sidebar-header { padding: 12px 12px 8px; }
|
/* Collapse button in sidebar header */
|
||||||
|
.sidebar-collapse {
|
||||||
|
position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
|
||||||
|
width: 24px; height: 24px; border-radius: 4px;
|
||||||
|
border: 1px solid var(--rs-border-subtle, #333);
|
||||||
|
background: var(--rs-bg-surface, #1e1e2e);
|
||||||
|
color: var(--rs-text-muted, #888);
|
||||||
|
font-size: 16px; line-height: 1;
|
||||||
|
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
||||||
|
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
.sidebar-collapse:hover {
|
||||||
|
color: var(--rs-text-primary);
|
||||||
|
border-color: var(--rs-primary, #6366f1);
|
||||||
|
background: var(--rs-bg-hover, #252538);
|
||||||
|
}
|
||||||
|
/* Reopen tab on left edge */
|
||||||
|
.sidebar-reopen {
|
||||||
|
position: absolute; left: 0; top: 50%; transform: translateY(-50%);
|
||||||
|
width: 20px; height: 48px; z-index: 10;
|
||||||
|
border: 1px solid var(--rs-border-subtle, #333);
|
||||||
|
border-left: none;
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
background: var(--rs-bg-surface, #1e1e2e);
|
||||||
|
color: var(--rs-text-muted, #888);
|
||||||
|
font-size: 18px; line-height: 1;
|
||||||
|
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
||||||
|
opacity: 0; pointer-events: none;
|
||||||
|
transition: opacity 0.15s ease, color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
.sidebar-reopen:hover {
|
||||||
|
color: var(--rs-text-primary);
|
||||||
|
background: var(--rs-bg-hover, #252538);
|
||||||
|
}
|
||||||
|
.sidebar-header { padding: 12px 12px 8px; position: relative; }
|
||||||
.sidebar-search {
|
.sidebar-search {
|
||||||
width: 100%; padding: 8px 12px; border-radius: 6px;
|
width: 100%; padding: 8px 36px 8px 12px; border-radius: 6px;
|
||||||
border: 1px solid var(--rs-input-border); background: var(--rs-input-bg);
|
border: 1px solid var(--rs-input-border); background: var(--rs-input-bg);
|
||||||
color: var(--rs-input-text); font-size: 13px; font-family: inherit;
|
color: var(--rs-input-text); font-size: 13px; font-family: inherit;
|
||||||
transition: border-color 0.15s;
|
transition: border-color 0.15s;
|
||||||
|
|
@ -3550,8 +3670,8 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
}
|
}
|
||||||
/* Sidebar fills screen width */
|
/* Sidebar fills screen width */
|
||||||
.notes-sidebar { width: 100%; position: static; transform: none; box-shadow: none; }
|
.notes-sidebar { width: 100%; position: static; transform: none; box-shadow: none; }
|
||||||
/* Hide old overlay FAB (no longer needed) */
|
/* Hide old overlay FAB + desktop collapse on mobile */
|
||||||
.mobile-sidebar-toggle, .sidebar-overlay { display: none !important; }
|
.mobile-sidebar-toggle, .sidebar-overlay, .sidebar-collapse, .sidebar-reopen { display: none !important; }
|
||||||
/* Hide empty state on mobile — user sees doc list */
|
/* Hide empty state on mobile — user sees doc list */
|
||||||
.editor-empty-state { display: none; }
|
.editor-empty-state { display: none; }
|
||||||
/* Show back bar */
|
/* Show back bar */
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,45 @@ routes.get("/api/feed", (c) =>
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ── API: Threads (read-only, for cross-rApp consumption) ──
|
||||||
|
|
||||||
|
routes.get("/api/threads", (c) => {
|
||||||
|
const space = c.req.param("space") || "demo";
|
||||||
|
const dataSpace = c.get("effectiveSpace") || space;
|
||||||
|
const doc = ensureDoc(dataSpace);
|
||||||
|
const threads = Object.values(doc.threads || {}).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
||||||
|
return c.json({ threads, count: threads.length });
|
||||||
|
});
|
||||||
|
|
||||||
|
routes.get("/api/threads/:id", (c) => {
|
||||||
|
const space = c.req.param("space") || "demo";
|
||||||
|
const dataSpace = c.get("effectiveSpace") || space;
|
||||||
|
const id = c.req.param("id");
|
||||||
|
const thread = getThreadFromDoc(dataSpace, id);
|
||||||
|
if (!thread) return c.json({ error: "Thread not found" }, 404);
|
||||||
|
return c.json(thread);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── API: Campaigns (read-only, for cross-rApp consumption) ──
|
||||||
|
|
||||||
|
routes.get("/api/campaigns", (c) => {
|
||||||
|
const space = c.req.param("space") || "demo";
|
||||||
|
const dataSpace = c.get("effectiveSpace") || space;
|
||||||
|
const doc = ensureDoc(dataSpace);
|
||||||
|
const campaigns = Object.values(doc.campaigns || {}).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
||||||
|
return c.json({ campaigns, count: campaigns.length });
|
||||||
|
});
|
||||||
|
|
||||||
|
routes.get("/api/campaigns/:id", (c) => {
|
||||||
|
const space = c.req.param("space") || "demo";
|
||||||
|
const dataSpace = c.get("effectiveSpace") || space;
|
||||||
|
const id = c.req.param("id");
|
||||||
|
const doc = ensureDoc(dataSpace);
|
||||||
|
const campaign = doc.campaigns?.[id];
|
||||||
|
if (!campaign) return c.json({ error: "Campaign not found" }, 404);
|
||||||
|
return c.json(campaign);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Image API routes (server-side, need filesystem + FAL_KEY) ──
|
// ── Image API routes (server-side, need filesystem + FAL_KEY) ──
|
||||||
|
|
||||||
routes.post("/api/threads/:id/image", async (c) => {
|
routes.post("/api/threads/:id/image", async (c) => {
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,8 @@ include action markers in your response. Each marker is on its own line:
|
||||||
Use "$1", "$2", etc. as ref values when creating shapes, then reference them in subsequent connect actions.
|
Use "$1", "$2", etc. as ref values when creating shapes, then reference them in subsequent connect actions.
|
||||||
Available shape types: folk-markdown, folk-wrapper, folk-image-gen, folk-video-gen, folk-prompt,
|
Available shape types: folk-markdown, folk-wrapper, folk-image-gen, folk-video-gen, folk-prompt,
|
||||||
folk-embed, folk-calendar, folk-map, folk-chat, folk-slide, folk-obs-note, folk-workflow-block,
|
folk-embed, folk-calendar, folk-map, folk-chat, folk-slide, folk-obs-note, folk-workflow-block,
|
||||||
folk-social-post, folk-splat, folk-drawfast, folk-rapp, folk-feed.
|
folk-social-post, folk-social-thread, folk-social-campaign, folk-social-newsletter,
|
||||||
|
folk-splat, folk-drawfast, folk-rapp, folk-feed.
|
||||||
|
|
||||||
## Transforms
|
## Transforms
|
||||||
When the user asks to align, distribute, or arrange selected shapes:
|
When the user asks to align, distribute, or arrange selected shapes:
|
||||||
|
|
@ -319,6 +320,9 @@ analyze it and classify each distinct piece into the most appropriate canvas sha
|
||||||
- Locations / addresses / places → folk-map (set query prop)
|
- Locations / addresses / places → folk-map (set query prop)
|
||||||
- Action items / TODOs / tasks → folk-workflow-block (set label, blockType:"action" props)
|
- Action items / TODOs / tasks → folk-workflow-block (set label, blockType:"action" props)
|
||||||
- Social media content / posts → folk-social-post (set content prop)
|
- Social media content / posts → folk-social-post (set content prop)
|
||||||
|
- Tweet threads / multi-post threads → folk-social-thread (set title, tweets props)
|
||||||
|
- Marketing campaigns / content plans → folk-social-campaign (set title, description, platforms props)
|
||||||
|
- Newsletters / email campaigns → folk-social-newsletter (set subject, listName props)
|
||||||
- Decisions / polls / questions for voting → folk-choice-vote (set question prop)
|
- Decisions / polls / questions for voting → folk-choice-vote (set question prop)
|
||||||
- Everything else (prose, notes, transcripts, summaries) → folk-markdown (set content prop in markdown format)
|
- Everything else (prose, notes, transcripts, summaries) → folk-markdown (set content prop in markdown format)
|
||||||
|
|
||||||
|
|
@ -340,7 +344,8 @@ Return a JSON object with:
|
||||||
const KNOWN_TRIAGE_SHAPES = new Set([
|
const KNOWN_TRIAGE_SHAPES = new Set([
|
||||||
"folk-markdown", "folk-embed", "folk-image", "folk-bookmark",
|
"folk-markdown", "folk-embed", "folk-image", "folk-bookmark",
|
||||||
"folk-calendar", "folk-map",
|
"folk-calendar", "folk-map",
|
||||||
"folk-workflow-block", "folk-social-post", "folk-choice-vote",
|
"folk-workflow-block", "folk-social-post", "folk-social-thread",
|
||||||
|
"folk-social-campaign", "folk-social-newsletter", "folk-choice-vote",
|
||||||
"folk-prompt", "folk-image-gen", "folk-slide",
|
"folk-prompt", "folk-image-gen", "folk-slide",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue