feat(rnotes): add collab status bar, sidebar indicator, mobile UX polish
Adds a visible collab status bar between toolbar and editor showing connection state (live editing with peer count, sync enabled, or offline). Sidebar footer now shows a live collab indicator dot. Mobile sidebar auto-closes when selecting a note. Mobile FAB button now shows "Docs" label. Bumps cache version to v=5. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a7eda3c53f
commit
32524fdf00
|
|
@ -287,7 +287,7 @@ class FolkNotesApp extends HTMLElement {
|
||||||
// Mobile sidebar toggle
|
// Mobile sidebar toggle
|
||||||
const mobileToggle = document.createElement('button');
|
const mobileToggle = document.createElement('button');
|
||||||
mobileToggle.className = 'mobile-sidebar-toggle';
|
mobileToggle.className = 'mobile-sidebar-toggle';
|
||||||
mobileToggle.innerHTML = '\u2630';
|
mobileToggle.innerHTML = '<span class="mobile-toggle-icon">\u{1F4C4}</span><span class="mobile-toggle-label">Docs</span>';
|
||||||
mobileToggle.addEventListener('click', () => {
|
mobileToggle.addEventListener('click', () => {
|
||||||
this.sidebarOpen = !this.sidebarOpen;
|
this.sidebarOpen = !this.sidebarOpen;
|
||||||
this.navZone.querySelector('.notes-sidebar')?.classList.toggle('open', this.sidebarOpen);
|
this.navZone.querySelector('.notes-sidebar')?.classList.toggle('open', this.sidebarOpen);
|
||||||
|
|
@ -1049,6 +1049,13 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
private async openNote(noteId: string, notebookId: string) {
|
private async openNote(noteId: string, notebookId: string) {
|
||||||
const isDemo = this.space === "demo";
|
const isDemo = this.space === "demo";
|
||||||
|
|
||||||
|
// Auto-close sidebar on mobile
|
||||||
|
if (window.innerWidth <= 768) {
|
||||||
|
this.sidebarOpen = false;
|
||||||
|
this.navZone.querySelector('.notes-sidebar')?.classList.remove('open');
|
||||||
|
this.shadow.querySelector('.sidebar-overlay')?.classList.remove('open');
|
||||||
|
}
|
||||||
|
|
||||||
// Expand notebook if not expanded
|
// Expand notebook if not expanded
|
||||||
if (!this.expandedNotebooks.has(notebookId)) {
|
if (!this.expandedNotebooks.has(notebookId)) {
|
||||||
this.expandedNotebooks.add(notebookId);
|
this.expandedNotebooks.add(notebookId);
|
||||||
|
|
@ -1210,6 +1217,10 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
<div class="editor-wrapper">
|
<div class="editor-wrapper">
|
||||||
<input class="editable-title" id="note-title-input" value="${this.esc(note.title)}" placeholder="Note title...">
|
<input class="editable-title" id="note-title-input" value="${this.esc(note.title)}" placeholder="Note title...">
|
||||||
${isEditable ? this.renderToolbar() : ''}
|
${isEditable ? this.renderToolbar() : ''}
|
||||||
|
<div class="collab-status-bar" id="collab-status-bar" style="display:none">
|
||||||
|
<span class="collab-status-dot"></span>
|
||||||
|
<span class="collab-status-text"></span>
|
||||||
|
</div>
|
||||||
<div class="tiptap-container" id="tiptap-container"></div>
|
<div class="tiptap-container" id="tiptap-container"></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -1333,8 +1344,16 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
name: session.username || 'Anonymous',
|
name: session.username || 'Anonymous',
|
||||||
color: this.userColor(session.userId || 'anon'),
|
color: this.userColor(session.userId || 'anon'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update collab status bar when peers change
|
||||||
|
this.yjsProvider.awareness.on('update', () => {
|
||||||
|
this.updatePeersIndicator();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initial collab status bar update
|
||||||
|
this.updatePeersIndicator();
|
||||||
|
|
||||||
// Periodic plaintext sync to Automerge (for search indexing)
|
// Periodic plaintext sync to Automerge (for search indexing)
|
||||||
this.yjsPlainTextTimer = setInterval(() => {
|
this.yjsPlainTextTimer = setInterval(() => {
|
||||||
if (!this.editor || !this.editorNoteId) return;
|
if (!this.editor || !this.editorNoteId) return;
|
||||||
|
|
@ -2382,13 +2401,19 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
|
|
||||||
private updatePeersIndicator() {
|
private updatePeersIndicator() {
|
||||||
const peersEl = this.shadow.getElementById('collab-peers');
|
const peersEl = this.shadow.getElementById('collab-peers');
|
||||||
|
const statusBar = this.shadow.getElementById('collab-status-bar');
|
||||||
|
|
||||||
if (!peersEl || !this.yjsProvider) {
|
if (!peersEl || !this.yjsProvider) {
|
||||||
if (peersEl) peersEl.style.display = 'none';
|
if (peersEl) peersEl.style.display = 'none';
|
||||||
|
if (statusBar) statusBar.style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const connected = this.yjsProvider.isConnected;
|
||||||
const states = this.yjsProvider.awareness.getStates();
|
const states = this.yjsProvider.awareness.getStates();
|
||||||
const peerCount = states.size - 1; // Exclude self
|
const peerCount = states.size - 1; // Exclude self
|
||||||
|
|
||||||
|
// Toolbar peer dots
|
||||||
if (peerCount > 0) {
|
if (peerCount > 0) {
|
||||||
peersEl.style.display = 'inline-flex';
|
peersEl.style.display = 'inline-flex';
|
||||||
peersEl.innerHTML = '';
|
peersEl.innerHTML = '';
|
||||||
|
|
@ -2413,6 +2438,38 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
} else {
|
} else {
|
||||||
peersEl.style.display = 'none';
|
peersEl.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collab status bar
|
||||||
|
if (statusBar) {
|
||||||
|
statusBar.style.display = 'flex';
|
||||||
|
const dot = statusBar.querySelector('.collab-status-dot') as HTMLElement;
|
||||||
|
const text = statusBar.querySelector('.collab-status-text') as HTMLElement;
|
||||||
|
if (!dot || !text) return;
|
||||||
|
|
||||||
|
if (!connected) {
|
||||||
|
dot.className = 'collab-status-dot offline';
|
||||||
|
text.textContent = 'Offline \u2014 changes will sync when reconnected';
|
||||||
|
} else if (peerCount > 0) {
|
||||||
|
dot.className = 'collab-status-dot live';
|
||||||
|
const names: string[] = [];
|
||||||
|
for (const [clientId, state] of states) {
|
||||||
|
if (clientId === this.ydoc?.clientID) continue;
|
||||||
|
names.push(state.user?.name || 'Anonymous');
|
||||||
|
}
|
||||||
|
text.textContent = `Live editing \u00B7 ${peerCount} collaborator${peerCount > 1 ? 's' : ''}`;
|
||||||
|
text.title = names.join(', ');
|
||||||
|
} else {
|
||||||
|
dot.className = 'collab-status-dot synced';
|
||||||
|
text.textContent = 'Live sync enabled';
|
||||||
|
text.title = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sidebar collab dot
|
||||||
|
const sidebarDot = this.navZone.querySelector('.sidebar-collab-dot') as HTMLElement;
|
||||||
|
if (sidebarDot) {
|
||||||
|
sidebarDot.className = `sidebar-collab-dot ${connected ? 'connected' : 'disconnected'}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ──
|
// ── Helpers ──
|
||||||
|
|
@ -2517,6 +2574,10 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
<div class="sidebar-tree">
|
<div class="sidebar-tree">
|
||||||
${treeHtml}
|
${treeHtml}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sidebar-collab-info" title="All editors in this space see changes in real-time. Share the space link to collaborate.">
|
||||||
|
<span class="sidebar-collab-dot ${this.yjsProvider?.isConnected ? 'connected' : 'disconnected'}"></span>
|
||||||
|
<span>Live Collab</span>
|
||||||
|
</div>
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<button class="sidebar-footer-btn" id="btn-import-export">Import / Export</button>
|
<button class="sidebar-footer-btn" id="btn-import-export">Import / Export</button>
|
||||||
<button class="sidebar-footer-btn" id="btn-tour">Tour</button>
|
<button class="sidebar-footer-btn" id="btn-tour">Tour</button>
|
||||||
|
|
@ -2877,6 +2938,19 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
}
|
}
|
||||||
.sidebar-footer-btn:hover { border-color: var(--rs-border-strong); color: var(--rs-text-primary); }
|
.sidebar-footer-btn:hover { border-color: var(--rs-border-strong); color: var(--rs-text-primary); }
|
||||||
|
|
||||||
|
/* Sidebar collab info */
|
||||||
|
.sidebar-collab-info {
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
padding: 6px 12px; font-size: 11px; color: var(--rs-text-muted);
|
||||||
|
border-top: 1px solid var(--rs-border-subtle); cursor: default;
|
||||||
|
}
|
||||||
|
.sidebar-collab-dot {
|
||||||
|
width: 7px; height: 7px; border-radius: 50%;
|
||||||
|
flex-shrink: 0; background: #9ca3af;
|
||||||
|
}
|
||||||
|
.sidebar-collab-dot.connected { background: #22c55e; }
|
||||||
|
.sidebar-collab-dot.disconnected { background: #9ca3af; }
|
||||||
|
|
||||||
/* Sidebar search results */
|
/* Sidebar search results */
|
||||||
.sidebar-search-results { padding: 4px 0; }
|
.sidebar-search-results { padding: 4px 0; }
|
||||||
.sidebar-search-result {
|
.sidebar-search-result {
|
||||||
|
|
@ -2906,11 +2980,14 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
/* Mobile sidebar */
|
/* Mobile sidebar */
|
||||||
.mobile-sidebar-toggle {
|
.mobile-sidebar-toggle {
|
||||||
display: none; position: fixed; bottom: 20px; left: 20px; z-index: 198;
|
display: none; position: fixed; bottom: 20px; left: 20px; z-index: 198;
|
||||||
width: 44px; height: 44px; border-radius: 50%; border: none;
|
min-width: 44px; height: 44px; border-radius: 22px; border: none;
|
||||||
background: var(--rs-primary); color: #fff; font-size: 20px;
|
padding: 0 14px; gap: 4px;
|
||||||
|
background: var(--rs-primary); color: #fff; font-size: 13px;
|
||||||
cursor: pointer; box-shadow: var(--rs-shadow-md);
|
cursor: pointer; box-shadow: var(--rs-shadow-md);
|
||||||
align-items: center; justify-content: center;
|
align-items: center; justify-content: center;
|
||||||
}
|
}
|
||||||
|
.mobile-toggle-icon { font-size: 18px; }
|
||||||
|
.mobile-toggle-label { font-weight: 600; font-family: inherit; }
|
||||||
.sidebar-overlay {
|
.sidebar-overlay {
|
||||||
display: none; position: fixed; inset: 0;
|
display: none; position: fixed; inset: 0;
|
||||||
background: rgba(0,0,0,0.4); z-index: 199;
|
background: rgba(0,0,0,0.4); z-index: 199;
|
||||||
|
|
@ -3331,6 +3408,25 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
||||||
height: 1.2em;
|
height: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Collaboration: Status Bar ── */
|
||||||
|
.collab-status-bar {
|
||||||
|
display: none; align-items: center; gap: 8px;
|
||||||
|
padding: 4px 16px; height: 28px;
|
||||||
|
background: var(--rs-bg-hover, rgba(0,0,0,0.03));
|
||||||
|
border-top: 1px solid var(--rs-border-subtle);
|
||||||
|
border-bottom: 1px solid var(--rs-border-subtle);
|
||||||
|
font-size: 12px; color: var(--rs-text-muted);
|
||||||
|
}
|
||||||
|
.collab-status-dot {
|
||||||
|
width: 8px; height: 8px; border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
|
.collab-status-dot.live { background: #22c55e; }
|
||||||
|
.collab-status-dot.synced { background: #3b82f6; }
|
||||||
|
.collab-status-dot.offline { background: #9ca3af; }
|
||||||
|
.collab-status-text { white-space: nowrap; }
|
||||||
|
|
||||||
/* ── Collaboration: Peers Indicator ── */
|
/* ── Collaboration: Peers Indicator ── */
|
||||||
.collab-tools { display: flex; align-items: center; gap: 4px; }
|
.collab-tools { display: flex; align-items: center; gap: 4px; }
|
||||||
.collab-peers {
|
.collab-peers {
|
||||||
|
|
|
||||||
|
|
@ -1615,8 +1615,8 @@ routes.get("/", (c) => {
|
||||||
modules: getModuleInfoList(),
|
modules: getModuleInfoList(),
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
body: `<folk-notes-app space="${space}"></folk-notes-app>`,
|
body: `<folk-notes-app space="${space}"></folk-notes-app>`,
|
||||||
scripts: `<script type="module" src="/modules/rnotes/folk-notes-app.js?v=4"></script>`,
|
scripts: `<script type="module" src="/modules/rnotes/folk-notes-app.js?v=5"></script>`,
|
||||||
styles: `<link rel="stylesheet" href="/modules/rnotes/notes.css?v=4">`,
|
styles: `<link rel="stylesheet" href="/modules/rnotes/notes.css?v=5">`,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,10 @@ export class RSpaceYjsProvider {
|
||||||
return this.synced;
|
return this.synced;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isConnected(): boolean {
|
||||||
|
return this.connected;
|
||||||
|
}
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
// Remove local awareness state
|
// Remove local awareness state
|
||||||
removeAwarenessStates(
|
removeAwarenessStates(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue