Neon:ryan commited on
Commit
abcb14f
·
1 Parent(s): dd07b04

Enhance Canvas sidebar and deliverables functionality

Browse files

- Introduced a new sidebar structure for the Canvas page, allowing users to navigate between Insights, Workspace, and Deliverables with improved organization.
- Implemented expandable sections for deliverable projects, enabling users to view and manage project details more effectively.
- Added an auto-save indicator next to project titles to inform users of save status in real-time.
- Updated CSS styles for the sidebar and deliverables view to enhance visual consistency and user experience.

These changes significantly improve the usability and functionality of the Canvas interface, making project management more intuitive.

phd-advisor-frontend/src/components/Sidebar.js CHANGED
@@ -11,7 +11,9 @@ import {
11
  User,
12
  Settings,
13
  PanelLeft,
14
- FileText
 
 
15
  } from 'lucide-react';
16
  import CopyrightNotice from './CopyrightNotice';
17
  import '../styles/Sidebar.css';
@@ -30,9 +32,22 @@ const Sidebar = ({
30
  refreshTrigger,
31
  onCurrentSessionDeleted,
32
  pageContext = 'chat',
33
- canvasItems = []
 
 
 
34
  }) => {
35
  const isOnCanvas = pageContext === 'canvas';
 
 
 
 
 
 
 
 
 
 
36
  const [chatSessions, setChatSessions] = useState([]);
37
  const [searchTerm, setSearchTerm] = useState('');
38
  const [isLoading, setIsLoading] = useState(true);
@@ -260,7 +275,11 @@ const Sidebar = ({
260
  <Search size={16} className="search-icon" />
261
  <input
262
  type="text"
263
- placeholder={isOnCanvas ? 'Search widgets...' : 'Search chats...'}
 
 
 
 
264
  value={searchTerm}
265
  onChange={(e) => setSearchTerm(e.target.value)}
266
  className="search-input"
@@ -279,39 +298,123 @@ const Sidebar = ({
279
  </div>
280
  )}
281
 
282
- {/* Canvas widgets list (when on canvas) */}
283
  {isOnCanvas ? (
284
- <div className="chat-sessions">
285
- {(() => {
286
- const filtered = canvasItems.filter(it =>
287
- !searchTerm || it.label.toLowerCase().includes(searchTerm.toLowerCase())
288
- );
289
- if (filtered.length === 0) {
290
- return (
291
- <div className="no-sessions">
292
- {!isCollapsed && (searchTerm ? 'No widgets match' : 'No widgets yet')}
293
- </div>
294
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  }
296
  return (
297
  <div className="sessions-list">
298
- {filtered.map((it) => (
299
- <div
300
- key={it.id}
301
- className={`session-item ${isCollapsed ? 'collapsed' : ''}`}
302
- onClick={it.onClick}
303
- title={isCollapsed ? it.label : ''}
304
- >
305
  <div className="session-content">
306
- <div className="session-icon">
307
- <FileText size={16} />
 
 
308
  </div>
309
- {!isCollapsed && (
310
- <div className="session-details">
311
- <div className="session-title">{it.label}</div>
312
- {it.sub && <div className="session-meta"><span>{it.sub}</span></div>}
313
- </div>
314
- )}
315
  </div>
316
  </div>
317
  ))}
 
11
  User,
12
  Settings,
13
  PanelLeft,
14
+ FileText,
15
+ ChevronRight,
16
+ Clock
17
  } from 'lucide-react';
18
  import CopyrightNotice from './CopyrightNotice';
19
  import '../styles/Sidebar.css';
 
32
  refreshTrigger,
33
  onCurrentSessionDeleted,
34
  pageContext = 'chat',
35
+ canvasItems = [],
36
+ canvasSubview = 'workspace',
37
+ widgetGroups = [],
38
+ deliverableProjects = [],
39
  }) => {
40
  const isOnCanvas = pageContext === 'canvas';
41
+ const [expanded, setExpanded] = useState(() => {
42
+ try { return JSON.parse(localStorage.getItem('sidebar-expanded-v1') || '{}'); } catch { return {}; }
43
+ });
44
+ const toggleExpanded = (key) => {
45
+ setExpanded(prev => {
46
+ const next = { ...prev, [key]: !prev[key] };
47
+ localStorage.setItem('sidebar-expanded-v1', JSON.stringify(next));
48
+ return next;
49
+ });
50
+ };
51
  const [chatSessions, setChatSessions] = useState([]);
52
  const [searchTerm, setSearchTerm] = useState('');
53
  const [isLoading, setIsLoading] = useState(true);
 
275
  <Search size={16} className="search-icon" />
276
  <input
277
  type="text"
278
+ placeholder={
279
+ isOnCanvas
280
+ ? (canvasSubview === 'deliverables' ? 'Search drafts...' : 'Search widgets...')
281
+ : 'Search chats...'
282
+ }
283
  value={searchTerm}
284
  onChange={(e) => setSearchTerm(e.target.value)}
285
  className="search-input"
 
298
  </div>
299
  )}
300
 
301
+ {/* Canvas sidebar subview-aware (Insights / Workspace / Deliverables) */}
302
  {isOnCanvas ? (
303
+ <div className="canvas-sidebar-menu">
304
+ {!isCollapsed && (() => {
305
+ const q = searchTerm.toLowerCase();
306
+
307
+ // ---------- DELIVERABLES: project list with expandable section dropdown ----------
308
+ if (canvasSubview === 'deliverables') {
309
+ const projects = deliverableProjects.filter(p =>
310
+ !q || p.name.toLowerCase().includes(q) || p.sections.some(s => s.name.toLowerCase().includes(q))
 
 
311
  );
312
+ if (projects.length === 0) {
313
+ return (
314
+ <div className="no-sessions">
315
+ {searchTerm ? 'No drafts match' : 'No drafts yet — create one in Deliverables'}
316
+ </div>
317
+ );
318
+ }
319
+ return projects.map(p => {
320
+ const open = expanded[`p-${p.id}`] ?? p.isActive;
321
+ const totalWords = p.sections.reduce((s, x) => s + x.wc, 0);
322
+ return (
323
+ <div key={p.id} className={`csm-project ${p.isActive ? 'active' : ''}`}>
324
+ <button
325
+ className="csm-project-head"
326
+ onClick={() => toggleExpanded(`p-${p.id}`)}
327
+ >
328
+ <ChevronRight size={12} className={`csm-chevron ${open ? 'open' : ''}`}/>
329
+ <FileText size={13}/>
330
+ <span className="csm-project-name">{p.name}</span>
331
+ {p.versions > 0 && (
332
+ <span className="csm-versions" title={`${p.versions} version${p.versions === 1 ? '' : 's'} saved`}>
333
+ <Clock size={10}/>{p.versions}
334
+ </span>
335
+ )}
336
+ </button>
337
+ {open && (
338
+ <div className="csm-project-body">
339
+ <button className="csm-row" onClick={p.onOpen}>
340
+ <span className="csm-row-icon">📂</span>
341
+ <span>Open editor</span>
342
+ </button>
343
+ {p.sections.map(s => (
344
+ <button key={s.id} className="csm-row csm-row-section" onClick={s.onClick}>
345
+ <span className="csm-row-bullet"/>
346
+ <span className="csm-row-label">{s.name}</span>
347
+ {s.wc > 0 && <span className="csm-row-meta">{s.wc}</span>}
348
+ </button>
349
+ ))}
350
+ <div className="csm-row csm-row-foot">
351
+ <Clock size={11}/>
352
+ <span>{p.versions} version{p.versions === 1 ? '' : 's'} · auto-saved</span>
353
+ </div>
354
+ <div className="csm-row csm-row-foot">
355
+ <span style={{ color: 'var(--text-tertiary, #9CA3AF)' }}>{totalWords} words total</span>
356
+ </div>
357
+ </div>
358
+ )}
359
+ </div>
360
+ );
361
+ });
362
+ }
363
+
364
+ // ---------- WORKSPACE: widgets grouped by category ----------
365
+ if (canvasSubview === 'workspace') {
366
+ const groups = widgetGroups
367
+ .map(g => ({
368
+ ...g,
369
+ items: g.items.filter(it => !q || it.label.toLowerCase().includes(q)),
370
+ }))
371
+ .filter(g => g.items.length > 0);
372
+ if (groups.length === 0) {
373
+ return (
374
+ <div className="no-sessions">
375
+ {searchTerm ? 'No widgets match' : 'Workspace is empty — add widgets'}
376
+ </div>
377
+ );
378
+ }
379
+ return groups.map(g => {
380
+ const open = expanded[`g-${g.id}`] ?? true;
381
+ return (
382
+ <div key={g.id} className="csm-group">
383
+ <button className="csm-group-head" onClick={() => toggleExpanded(`g-${g.id}`)}>
384
+ <ChevronRight size={11} className={`csm-chevron ${open ? 'open' : ''}`}/>
385
+ <span className="csm-group-name">{g.label}</span>
386
+ <span className="csm-group-count">{g.items.length}</span>
387
+ </button>
388
+ {open && (
389
+ <div className="csm-group-body">
390
+ {g.items.map(it => (
391
+ <button key={it.id} className={`csm-row ${it.critic ? 'critic' : ''}`} onClick={it.onClick}>
392
+ <span className="csm-row-bullet"/>
393
+ <span className="csm-row-label">{it.label}</span>
394
+ </button>
395
+ ))}
396
+ </div>
397
+ )}
398
+ </div>
399
+ );
400
+ });
401
+ }
402
+
403
+ // ---------- INSIGHTS / fallback: keep flat list ----------
404
+ const items = canvasItems.filter(it => !q || it.label.toLowerCase().includes(q));
405
+ if (items.length === 0) {
406
+ return <div className="no-sessions">{searchTerm ? 'No matches' : 'Nothing here yet'}</div>;
407
  }
408
  return (
409
  <div className="sessions-list">
410
+ {items.map((it) => (
411
+ <div key={it.id} className="session-item" onClick={it.onClick}>
 
 
 
 
 
412
  <div className="session-content">
413
+ <div className="session-icon"><FileText size={16}/></div>
414
+ <div className="session-details">
415
+ <div className="session-title">{it.label}</div>
416
+ {it.sub && <div className="session-meta"><span>{it.sub}</span></div>}
417
  </div>
 
 
 
 
 
 
418
  </div>
419
  </div>
420
  ))}
phd-advisor-frontend/src/components/canvas/CanvasDeliverables.js CHANGED
@@ -27,7 +27,7 @@ const newId = (p) => p + Math.random().toString(36).slice(2, 8);
27
  // ============================================================================
28
  // Templates
29
  // ============================================================================
30
- const TEMPLATES = [
31
  {
32
  id: 'research-paper',
33
  name: 'Research Paper',
@@ -509,11 +509,14 @@ const DeliverablesView = ({ allStates }) => {
509
  <button className="btn btn-ghost" style={{ padding: '4px 8px', fontSize: 12, marginBottom: 4, color: 'var(--canvas-text-3)' }} onClick={closeProject}>
510
  <Icon name="back" size={12}/>All drafts
511
  </button>
512
- <input
513
- className="page-title page-title-editable"
514
- value={project.name}
515
- onChange={(e) => renameProject(e.target.value)}
516
- />
 
 
 
517
  <div className="page-sub">
518
  {template.name} · {totalWords} / {totalTarget} words · ~{readingMinutes(totalWords)} min read · {template.sections.length} {template.mode === 'slides' ? 'slides' : 'sections'}
519
  </div>
@@ -804,6 +807,45 @@ const DeliverablesView = ({ allStates }) => {
804
  );
805
  };
806
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
807
  // ============================================================================
808
  // RichBlock — Notion-style click-to-edit + Docs-style floating toolbar.
809
  // • Idle: shows rendered markdown (with KaTeX math) — looks like a real doc
 
27
  // ============================================================================
28
  // Templates
29
  // ============================================================================
30
+ export const TEMPLATES = [
31
  {
32
  id: 'research-paper',
33
  name: 'Research Paper',
 
509
  <button className="btn btn-ghost" style={{ padding: '4px 8px', fontSize: 12, marginBottom: 4, color: 'var(--canvas-text-3)' }} onClick={closeProject}>
510
  <Icon name="back" size={12}/>All drafts
511
  </button>
512
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
513
+ <input
514
+ className="page-title page-title-editable"
515
+ value={project.name}
516
+ onChange={(e) => renameProject(e.target.value)}
517
+ />
518
+ <SaveIndicator project={project}/>
519
+ </div>
520
  <div className="page-sub">
521
  {template.name} · {totalWords} / {totalTarget} words · ~{readingMinutes(totalWords)} min read · {template.sections.length} {template.mode === 'slides' ? 'slides' : 'sections'}
522
  </div>
 
807
  );
808
  };
809
 
810
+ // ============================================================================
811
+ // Auto-save indicator. Drafts persist to localStorage on every keystroke,
812
+ // so we show a transient "Saving…" pill when the project changes, settling
813
+ // to "Saved · Xs ago" while idle.
814
+ // ============================================================================
815
+ function SaveIndicator({ project }) {
816
+ const [savedAt, setSavedAt] = useState(Date.now());
817
+ const [pulse, setPulse] = useState(false);
818
+ const sectionsKey = JSON.stringify(project?.sections || {});
819
+ useEffect(() => {
820
+ setPulse(true);
821
+ const t1 = setTimeout(() => setPulse(false), 300);
822
+ const t2 = setTimeout(() => setSavedAt(Date.now()), 350);
823
+ return () => { clearTimeout(t1); clearTimeout(t2); };
824
+ }, [sectionsKey]);
825
+
826
+ // Tick the relative-time string
827
+ const [, force] = useState(0);
828
+ useEffect(() => {
829
+ const i = setInterval(() => force(n => n + 1), 5000);
830
+ return () => clearInterval(i);
831
+ }, []);
832
+
833
+ const ago = (() => {
834
+ const d = Math.floor((Date.now() - savedAt) / 1000);
835
+ if (d < 5) return 'just now';
836
+ if (d < 60) return `${d}s ago`;
837
+ if (d < 3600) return `${Math.floor(d / 60)}m ago`;
838
+ return `${Math.floor(d / 3600)}h ago`;
839
+ })();
840
+
841
+ return (
842
+ <span className={`save-indicator ${pulse ? 'saving' : ''}`}>
843
+ <span className="save-indicator-dot"/>
844
+ {pulse ? 'Saving…' : `Saved · ${ago}`}
845
+ </span>
846
+ );
847
+ }
848
+
849
  // ============================================================================
850
  // RichBlock — Notion-style click-to-edit + Docs-style floating toolbar.
851
  // • Idle: shows rendered markdown (with KaTeX math) — looks like a real doc
phd-advisor-frontend/src/pages/CanvasPage.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useCallback } from 'react';
2
  import { HelpCircle } from 'lucide-react';
3
  import { useTheme } from '../contexts/ThemeContext';
4
  import { useAppConfig } from '../contexts/AppConfigContext';
@@ -27,7 +27,7 @@ import {
27
  PaletteModal, CommandPaletteModal, GlobalSearchModal,
28
  } from '../components/canvas/CanvasModals';
29
  import CanvasWelcomeTour from '../components/canvas/CanvasWelcomeTour';
30
- import DeliverablesView from '../components/canvas/CanvasDeliverables';
31
  import '../styles/CanvasPage.css';
32
 
33
  const LAYOUT_KEY = 'canvas-layout-v2';
@@ -455,22 +455,72 @@ const CanvasPage = ({ user, authToken, onNavigateToHome, onNavigateToChat, onSig
455
  return () => window.removeEventListener('keydown', k);
456
  }, [closeModal, openCommandPalette, openGlobalSearch]);
457
 
458
- const canvasSidebarItems = layout.map(w => {
459
- const meta = WIDGET_CATALOG.find(m => m.type === w.type);
460
- return {
461
- id: w.id,
462
- label: meta?.name || w.type,
463
- sub: meta?.cat || '',
464
- onClick: () => {
465
- const el = document.querySelector(`[data-widget-id="${w.id}"]`);
466
- if (el) {
467
- el.scrollIntoView({ block: 'center', behavior: 'smooth' });
468
- el.style.boxShadow = '0 0 0 2px var(--canvas-accent), 0 0 24px var(--canvas-accent-glow)';
469
- setTimeout(() => { el.style.boxShadow = ''; }, 1400);
470
- }
471
- },
472
- };
473
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
 
475
  return (
476
  <div className="canvas-page-with-sidebar" data-canvas-theme={theme}>
@@ -485,7 +535,9 @@ const CanvasPage = ({ user, authToken, onNavigateToHome, onNavigateToChat, onSig
485
  onSelectSession={(id) => onNavigateToChat && onNavigateToChat(id)}
486
  onNewChat={() => onNavigateToChat && onNavigateToChat()}
487
  pageContext="canvas"
488
- canvasItems={canvasSidebarItems}
 
 
489
  />
490
  <div className={`canvas-main-area ${isSidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
491
  <div className="canvas-app-shell">
 
1
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
2
  import { HelpCircle } from 'lucide-react';
3
  import { useTheme } from '../contexts/ThemeContext';
4
  import { useAppConfig } from '../contexts/AppConfigContext';
 
27
  PaletteModal, CommandPaletteModal, GlobalSearchModal,
28
  } from '../components/canvas/CanvasModals';
29
  import CanvasWelcomeTour from '../components/canvas/CanvasWelcomeTour';
30
+ import DeliverablesView, { TEMPLATES as DELIVERABLE_TEMPLATES } from '../components/canvas/CanvasDeliverables';
31
  import '../styles/CanvasPage.css';
32
 
33
  const LAYOUT_KEY = 'canvas-layout-v2';
 
455
  return () => window.removeEventListener('keydown', k);
456
  }, [closeModal, openCommandPalette, openGlobalSearch]);
457
 
458
+ // Highlight a widget when picked from the sidebar
459
+ const flashScrollTo = (selector) => {
460
+ const el = document.querySelector(selector);
461
+ if (!el) return;
462
+ el.scrollIntoView({ block: 'center', behavior: 'smooth' });
463
+ el.style.boxShadow = '0 0 0 2px var(--canvas-accent), 0 0 24px var(--canvas-accent-glow)';
464
+ setTimeout(() => { el.style.boxShadow = ''; }, 1400);
465
+ };
466
+
467
+ // Workspace: group widgets by category for the new sidebar
468
+ const widgetGroups = useMemo(() => {
469
+ const groups = {};
470
+ layout.forEach(w => {
471
+ const meta = WIDGET_CATALOG.find(m => m.type === w.type);
472
+ if (!meta) return;
473
+ const cat = meta.cat;
474
+ (groups[cat] ||= { id: cat, label: cat, items: [] }).items.push({
475
+ id: w.id,
476
+ label: meta.name,
477
+ icon: meta.icon,
478
+ critic: meta.critic,
479
+ onClick: () => flashScrollTo(`[data-widget-id="${w.id}"]`),
480
+ });
481
+ });
482
+ // Order: critic last
483
+ const order = ['research', 'writing', 'project', 'wellness', 'career', 'data', 'practical', 'critic'];
484
+ return order.map(c => groups[c]).filter(Boolean);
485
+ }, [layout]);
486
+
487
+ // Deliverables: list of projects with sections + history actions
488
+ const deliverableProjects = useMemo(() => {
489
+ try {
490
+ const dStore = JSON.parse(localStorage.getItem('canvas-deliverables-v2') || '{}');
491
+ const projects = Object.values(dStore.projects || {});
492
+ return projects.map(p => {
493
+ const t = DELIVERABLE_TEMPLATES.find(x => x.id === p.templateId);
494
+ return {
495
+ id: p.id,
496
+ name: p.name,
497
+ icon: t?.icon || 'book',
498
+ versions: p.versions?.length || 0,
499
+ isActive: p.id === dStore.activeProjectId,
500
+ sections: (t?.sections || []).map(s => ({
501
+ id: s.id,
502
+ name: s.name,
503
+ wc: ((p.sections || {})[s.id] || '').trim().split(/\s+/).filter(Boolean).length,
504
+ onClick: () => {
505
+ if (p.id !== dStore.activeProjectId) {
506
+ // Open this project first; section scroll happens after a tick.
507
+ const next = { ...dStore, activeProjectId: p.id };
508
+ localStorage.setItem('canvas-deliverables-v2', JSON.stringify(next));
509
+ window.dispatchEvent(new Event('storage'));
510
+ }
511
+ setTimeout(() => flashScrollTo(`#notion-section-${s.id}`), 80);
512
+ },
513
+ })),
514
+ onOpen: () => {
515
+ const next = { ...dStore, activeProjectId: p.id };
516
+ localStorage.setItem('canvas-deliverables-v2', JSON.stringify(next));
517
+ window.dispatchEvent(new Event('storage'));
518
+ },
519
+ };
520
+ });
521
+ } catch { return []; }
522
+ // re-derive when view or layout changes (layout proxy for "user did something")
523
+ }, [view, layout, widgetStates]); // eslint-disable-line react-hooks/exhaustive-deps
524
 
525
  return (
526
  <div className="canvas-page-with-sidebar" data-canvas-theme={theme}>
 
535
  onSelectSession={(id) => onNavigateToChat && onNavigateToChat(id)}
536
  onNewChat={() => onNavigateToChat && onNavigateToChat()}
537
  pageContext="canvas"
538
+ canvasSubview={view}
539
+ widgetGroups={widgetGroups}
540
+ deliverableProjects={deliverableProjects}
541
  />
542
  <div className={`canvas-main-area ${isSidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
543
  <div className="canvas-app-shell">
phd-advisor-frontend/src/styles/CanvasPage.css CHANGED
@@ -1609,6 +1609,159 @@ body[data-canvas-theme="light"] .canvas-modal-backdrop .critic-meter .bar { back
1609
  .canvas-page-with-sidebar .paper-empty { color: #9ca3af; font-style: italic; font-size: 12px; }
1610
  .canvas-page-with-sidebar .paper-section { margin-bottom: 4px; }
1611
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1612
  /* ----- Deliverable project tabs (Overleaf-style multi-draft) ----- */
1613
  .canvas-page-with-sidebar .deliverable-projects {
1614
  display: flex;
@@ -1652,6 +1805,41 @@ body[data-canvas-theme="light"] .canvas-modal-backdrop .critic-meter .bar { back
1652
  .canvas-page-with-sidebar .deliverable-new-project summary::-webkit-details-marker { display: none; }
1653
  .canvas-page-with-sidebar .deliverable-new-project[open] summary { background: var(--canvas-surface-2); color: var(--canvas-text); }
1654
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1655
  /* Editable project title — looks like the page-title but is an input */
1656
  .canvas-page-with-sidebar .page-title-editable {
1657
  background: transparent;
 
1609
  .canvas-page-with-sidebar .paper-empty { color: #9ca3af; font-style: italic; font-size: 12px; }
1610
  .canvas-page-with-sidebar .paper-section { margin-bottom: 4px; }
1611
 
1612
+ /* ----- Canvas-mode app sidebar menu (Insights / Workspace / Deliverables) ----- */
1613
+ /* These rules live in the *app* sidebar (not the canvas-page-with-sidebar scope)
1614
+ because they apply to .sidebar from components.css when the user is on canvas. */
1615
+ .canvas-sidebar-menu {
1616
+ flex: 1;
1617
+ overflow-y: auto;
1618
+ padding: 8px 8px 16px;
1619
+ display: flex;
1620
+ flex-direction: column;
1621
+ gap: 4px;
1622
+ }
1623
+ .canvas-sidebar-menu .no-sessions {
1624
+ padding: 18px 12px;
1625
+ font-size: 12px;
1626
+ color: var(--text-tertiary, #9CA3AF);
1627
+ text-align: center;
1628
+ }
1629
+
1630
+ /* --- Workspace: widget groups --- */
1631
+ .canvas-sidebar-menu .csm-group { display: flex; flex-direction: column; gap: 1px; margin-bottom: 4px; }
1632
+ .canvas-sidebar-menu .csm-group-head {
1633
+ display: flex;
1634
+ align-items: center;
1635
+ gap: 6px;
1636
+ padding: 6px 8px;
1637
+ background: transparent;
1638
+ border: none;
1639
+ border-radius: 5px;
1640
+ font-family: inherit;
1641
+ font-size: 11px;
1642
+ text-transform: uppercase;
1643
+ letter-spacing: 0.06em;
1644
+ font-weight: 600;
1645
+ color: var(--text-tertiary, #9CA3AF);
1646
+ cursor: pointer;
1647
+ }
1648
+ .canvas-sidebar-menu .csm-group-head:hover { background: var(--bg-secondary, #f3f4f6); color: var(--text-secondary, #6b7280); }
1649
+ .dark .canvas-sidebar-menu .csm-group-head:hover { background: var(--bg-secondary-dark, #374151); }
1650
+ .canvas-sidebar-menu .csm-group-name { flex: 1; text-align: left; }
1651
+ .canvas-sidebar-menu .csm-group-count {
1652
+ font-family: 'JetBrains Mono', monospace;
1653
+ font-size: 10px;
1654
+ color: var(--text-tertiary, #9CA3AF);
1655
+ }
1656
+ .canvas-sidebar-menu .csm-group-body { display: flex; flex-direction: column; gap: 1px; padding: 2px 0 6px 8px; }
1657
+
1658
+ /* --- Deliverables: project blocks --- */
1659
+ .canvas-sidebar-menu .csm-project {
1660
+ display: flex;
1661
+ flex-direction: column;
1662
+ gap: 1px;
1663
+ border-radius: 6px;
1664
+ margin-bottom: 4px;
1665
+ }
1666
+ .canvas-sidebar-menu .csm-project.active { background: rgba(99, 102, 241, 0.06); }
1667
+ .dark .canvas-sidebar-menu .csm-project.active { background: rgba(129, 140, 248, 0.10); }
1668
+ .canvas-sidebar-menu .csm-project-head {
1669
+ display: flex;
1670
+ align-items: center;
1671
+ gap: 7px;
1672
+ padding: 8px 10px;
1673
+ background: transparent;
1674
+ border: none;
1675
+ border-radius: 5px;
1676
+ font-family: inherit;
1677
+ font-size: 13px;
1678
+ font-weight: 500;
1679
+ color: var(--text-primary, #111827);
1680
+ cursor: pointer;
1681
+ text-align: left;
1682
+ }
1683
+ .canvas-sidebar-menu .csm-project-head:hover { background: var(--bg-secondary, #f3f4f6); }
1684
+ .dark .canvas-sidebar-menu .csm-project-head { color: var(--text-primary-dark, #f9fafb); }
1685
+ .dark .canvas-sidebar-menu .csm-project-head:hover { background: var(--bg-secondary-dark, #374151); }
1686
+ .canvas-sidebar-menu .csm-project.active .csm-project-name { color: var(--accent-primary, #6366F1); font-weight: 600; }
1687
+ .canvas-sidebar-menu .csm-project-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1688
+ .canvas-sidebar-menu .csm-versions {
1689
+ display: inline-flex;
1690
+ align-items: center;
1691
+ gap: 3px;
1692
+ font-family: 'JetBrains Mono', monospace;
1693
+ font-size: 10px;
1694
+ color: var(--text-tertiary, #9CA3AF);
1695
+ padding: 1px 5px;
1696
+ border-radius: 3px;
1697
+ background: var(--bg-secondary, #f3f4f6);
1698
+ }
1699
+ .dark .canvas-sidebar-menu .csm-versions { background: var(--bg-secondary-dark, #374151); }
1700
+ .canvas-sidebar-menu .csm-project-body {
1701
+ padding: 2px 0 8px 22px;
1702
+ display: flex;
1703
+ flex-direction: column;
1704
+ gap: 1px;
1705
+ }
1706
+
1707
+ /* --- Shared row (used in groups and project bodies) --- */
1708
+ .canvas-sidebar-menu .csm-row {
1709
+ display: flex;
1710
+ align-items: center;
1711
+ gap: 8px;
1712
+ padding: 5px 10px 5px 6px;
1713
+ background: transparent;
1714
+ border: none;
1715
+ border-radius: 4px;
1716
+ font-family: inherit;
1717
+ font-size: 12.5px;
1718
+ color: var(--text-secondary, #6b7280);
1719
+ text-align: left;
1720
+ cursor: pointer;
1721
+ }
1722
+ .canvas-sidebar-menu .csm-row:hover {
1723
+ background: var(--bg-secondary, #f3f4f6);
1724
+ color: var(--text-primary, #111827);
1725
+ }
1726
+ .dark .canvas-sidebar-menu .csm-row { color: var(--text-secondary-dark, #9ca3af); }
1727
+ .dark .canvas-sidebar-menu .csm-row:hover { background: var(--bg-secondary-dark, #374151); color: var(--text-primary-dark, #f9fafb); }
1728
+ .canvas-sidebar-menu .csm-row.critic { color: #8B5CF6; }
1729
+ .dark .canvas-sidebar-menu .csm-row.critic { color: #A78BFA; }
1730
+ .canvas-sidebar-menu .csm-row-bullet {
1731
+ width: 4px;
1732
+ height: 4px;
1733
+ border-radius: 50%;
1734
+ background: currentColor;
1735
+ opacity: 0.45;
1736
+ flex-shrink: 0;
1737
+ margin-left: 4px;
1738
+ }
1739
+ .canvas-sidebar-menu .csm-row-icon { font-size: 11px; opacity: 0.85; }
1740
+ .canvas-sidebar-menu .csm-row-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1741
+ .canvas-sidebar-menu .csm-row-meta {
1742
+ font-family: 'JetBrains Mono', monospace;
1743
+ font-size: 9.5px;
1744
+ color: var(--text-tertiary, #9CA3AF);
1745
+ }
1746
+ .canvas-sidebar-menu .csm-row-foot {
1747
+ display: flex;
1748
+ align-items: center;
1749
+ gap: 6px;
1750
+ padding: 3px 8px;
1751
+ font-family: 'JetBrains Mono', monospace;
1752
+ font-size: 9.5px;
1753
+ color: var(--text-tertiary, #9CA3AF);
1754
+ cursor: default;
1755
+ }
1756
+ .canvas-sidebar-menu .csm-row-foot:hover { background: transparent; color: var(--text-tertiary, #9CA3AF); }
1757
+
1758
+ /* Chevron rotates open/closed */
1759
+ .canvas-sidebar-menu .csm-chevron {
1760
+ transition: transform 0.15s ease;
1761
+ color: var(--text-tertiary, #9CA3AF);
1762
+ }
1763
+ .canvas-sidebar-menu .csm-chevron.open { transform: rotate(90deg); }
1764
+
1765
  /* ----- Deliverable project tabs (Overleaf-style multi-draft) ----- */
1766
  .canvas-page-with-sidebar .deliverable-projects {
1767
  display: flex;
 
1805
  .canvas-page-with-sidebar .deliverable-new-project summary::-webkit-details-marker { display: none; }
1806
  .canvas-page-with-sidebar .deliverable-new-project[open] summary { background: var(--canvas-surface-2); color: var(--canvas-text); }
1807
 
1808
+ /* Auto-save indicator next to the project title */
1809
+ .canvas-page-with-sidebar .save-indicator {
1810
+ display: inline-flex;
1811
+ align-items: center;
1812
+ gap: 6px;
1813
+ font-size: 11.5px;
1814
+ color: var(--canvas-text-3);
1815
+ font-family: var(--canvas-mono);
1816
+ white-space: nowrap;
1817
+ padding: 4px 8px;
1818
+ border-radius: 999px;
1819
+ background: var(--canvas-surface-2);
1820
+ transition: background .2s, color .2s;
1821
+ }
1822
+ .canvas-page-with-sidebar .save-indicator-dot {
1823
+ width: 6px;
1824
+ height: 6px;
1825
+ border-radius: 50%;
1826
+ background: var(--canvas-ok);
1827
+ box-shadow: 0 0 6px rgba(16, 185, 129, 0.4);
1828
+ transition: background .2s;
1829
+ }
1830
+ .canvas-page-with-sidebar .save-indicator.saving {
1831
+ background: var(--canvas-accent-glow);
1832
+ color: var(--canvas-accent);
1833
+ }
1834
+ .canvas-page-with-sidebar .save-indicator.saving .save-indicator-dot {
1835
+ background: var(--canvas-accent);
1836
+ animation: save-pulse 0.6s ease infinite;
1837
+ }
1838
+ @keyframes save-pulse {
1839
+ 0%, 100% { opacity: 1; transform: scale(1); }
1840
+ 50% { opacity: 0.4; transform: scale(0.85); }
1841
+ }
1842
+
1843
  /* Editable project title — looks like the page-title but is an input */
1844
  .canvas-page-with-sidebar .page-title-editable {
1845
  background: transparent;