Neon:ryan commited on
Commit
e881b4c
·
1 Parent(s): 03d90a3

Enhance UI and functionality across components

Browse files

- AppHeader: Hide tab bar on home page and add a mobile dropdown for navigation.
- CanvasDeliverables: Add print/save as PDF functionality.
- CanvasWidgets: Introduce a shared empty-state component for consistent UI across widgets.
- CanvasPage: Implement a floating shortcut hint for keyboard shortcuts and enhance keyboard navigation.
- CSS: Add animations for widget transitions, improve focus styles, and implement print styles for cleaner output.

This update improves user experience by streamlining navigation and enhancing visual consistency across the application.

phd-advisor-frontend/src/components/AppHeader.js CHANGED
@@ -64,12 +64,34 @@ const AppHeader = ({
64
  </div>
65
  </div>
66
 
67
- <div className="canvas-tabs chat-view-tabs">
68
- <button className={`tab ${isOnChat ? 'active' : ''}`} onClick={onNavigateToChat}>Chat</button>
69
- <button className={`tab ${tabActive('insights') ? 'active' : ''}`} onClick={() => goToCanvas('insights')}>Insights</button>
70
- <button className={`tab ${tabActive('workspace') ? 'active' : ''}`} onClick={() => goToCanvas('workspace')}>Workspace</button>
71
- <button className={`tab ${tabActive('deliverables') ? 'active' : ''}`} onClick={() => goToCanvas('deliverables')}>Deliverables</button>
72
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  <div className="header-right">
75
  {children}
 
64
  </div>
65
  </div>
66
 
67
+ {/* Hide the view pill bar on the home page — home is a landing page,
68
+ not part of the chat canvas surface. */}
69
+ {!isOnHome && (
70
+ <div className="canvas-tabs chat-view-tabs">
71
+ <button className={`tab ${isOnChat ? 'active' : ''}`} onClick={onNavigateToChat}>Chat</button>
72
+ <button className={`tab ${tabActive('insights') ? 'active' : ''}`} onClick={() => goToCanvas('insights')}>Insights</button>
73
+ <button className={`tab ${tabActive('workspace') ? 'active' : ''}`} onClick={() => goToCanvas('workspace')}>Workspace</button>
74
+ <button className={`tab ${tabActive('deliverables') ? 'active' : ''}`} onClick={() => goToCanvas('deliverables')}>Deliverables</button>
75
+ </div>
76
+ )}
77
+
78
+ {/* Compact mobile dropdown — appears in place of the pill bar at narrow widths */}
79
+ {!isOnHome && (
80
+ <select
81
+ className="canvas-tabs-mobile"
82
+ value={isOnChat ? 'chat' : (canvasSub || 'workspace')}
83
+ onChange={(e) => {
84
+ const v = e.target.value;
85
+ if (v === 'chat') onNavigateToChat();
86
+ else goToCanvas(v);
87
+ }}
88
+ >
89
+ <option value="chat">Chat</option>
90
+ <option value="insights">Insights</option>
91
+ <option value="workspace">Workspace</option>
92
+ <option value="deliverables">Deliverables</option>
93
+ </select>
94
+ )}
95
 
96
  <div className="header-right">
97
  {children}
phd-advisor-frontend/src/components/canvas/CanvasDeliverables.js CHANGED
@@ -296,6 +296,7 @@ const DeliverablesView = ({ allStates }) => {
296
  <button onClick={() => exportAs('markdown')}>Markdown (.md)</button>
297
  <button onClick={() => exportAs('latex')}>LaTeX (.tex)</button>
298
  <button onClick={() => exportAs('html')}>HTML (.html)</button>
 
299
  </div>
300
  </details>
301
  </div>
 
296
  <button onClick={() => exportAs('markdown')}>Markdown (.md)</button>
297
  <button onClick={() => exportAs('latex')}>LaTeX (.tex)</button>
298
  <button onClick={() => exportAs('html')}>HTML (.html)</button>
299
+ <button onClick={() => window.print()}>Print / Save as PDF</button>
300
  </div>
301
  </details>
302
  </div>
phd-advisor-frontend/src/components/canvas/CanvasWidgets.js CHANGED
@@ -8,6 +8,18 @@ const fireToast = (msg, kind = 'success') =>
8
  const fireActivity = (source, msg) =>
9
  window.dispatchEvent(new CustomEvent('canvas-activity', { detail: { source, msg } }));
10
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  // Cross-widget drag-drop helpers — one mime type, JSON payload tagged by `kind`.
12
  const X_MIME = 'application/x-canvas-item';
13
  const setDragPayload = (e, kind, payload) => {
@@ -142,9 +154,11 @@ export function BibliographyWidget({ state, setState, openModal }) {
142
  </div>
143
  ))}
144
  {sorted.length === 0 && (
145
- <div style={{ padding: 18, textAlign: 'center', color: 'var(--canvas-text-3)', fontSize: 12 }}>
146
- No citations yet. Add one or drag a paper from the Reading Queue.
147
- </div>
 
 
148
  )}
149
  </div>
150
  </div>
@@ -595,7 +609,11 @@ export function DeadlinesWidget({ state, setState, openModal }) {
595
  );
596
  })}
597
  {enriched.length === 0 && (
598
- <div style={{ padding: 18, textAlign: 'center', color: 'var(--canvas-text-3)', fontSize: 12 }}>No deadlines.</div>
 
 
 
 
599
  )}
600
  </div>
601
  <button className="add-tiny" onClick={() => openModal('add-deadline', {
@@ -751,9 +769,11 @@ export function NotesWidget({ state, setState, openModal }) {
751
  </div>
752
  ))}
753
  {filtered.length === 0 && (
754
- <div style={{ padding: 18, textAlign: 'center', color: 'var(--canvas-text-3)', fontSize: 12 }}>
755
- {notes.length === 0 ? 'No notes. Capture quick thoughts, paper claims, or ideas.' : `No notes match "${search}"`}
756
- </div>
 
 
757
  )}
758
  </div>
759
  <button className="add-tiny" onClick={add}>+ New note</button>
@@ -816,7 +836,7 @@ export function HabitsWidget({ state, setState, openModal }) {
816
  );
817
  })}
818
  {habits.length === 0 && (
819
- <div style={{ padding: 14, textAlign: 'center', color: 'var(--canvas-text-3)', fontSize: 12 }}>No habits yet.</div>
820
  )}
821
  </div>
822
  <button className="add-tiny" onClick={add}>+ New habit</button>
@@ -855,7 +875,7 @@ export function GoalsWidget({ state, setState, openModal }) {
855
  </div>
856
  ))}
857
  {goals.length === 0 && (
858
- <div style={{ padding: 18, textAlign: 'center', color: 'var(--canvas-text-3)', fontSize: 12 }}>No goals yet.</div>
859
  )}
860
  </div>
861
  <button className="add-tiny" onClick={add}>+ New goal</button>
@@ -888,9 +908,7 @@ export function MeetingsWidget({ state, setState, openModal }) {
888
  </div>
889
  ))}
890
  {meetings.length === 0 && (
891
- <div style={{ padding: 18, textAlign: 'center', color: 'var(--canvas-text-3)', fontSize: 12 }}>
892
- No meetings logged.
893
- </div>
894
  )}
895
  </div>
896
  <button className="add-tiny" onClick={add}>+ Log meeting</button>
@@ -935,9 +953,7 @@ export function OutlineWidget({ state, setState }) {
935
  <>
936
  <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
937
  {items.length === 0 ? (
938
- <div style={{ padding: 18, textAlign: 'center', color: 'var(--canvas-text-3)', fontSize: 12 }}>
939
- Empty outline. Click + to add the first node.
940
- </div>
941
  ) : items.map((it, i) => {
942
  const hasChildren = items[i + 1] && items[i + 1].depth > it.depth;
943
  const isCollapsed = !!expanded[it.id] && hasChildren;
@@ -1054,9 +1070,7 @@ export function HighlightsWidget({ state, setState }) {
1054
  </div>
1055
  ))}
1056
  {items.length === 0 && (
1057
- <div style={{ padding: 18, textAlign: 'center', color: 'var(--canvas-text-3)', fontSize: 12 }}>
1058
- No quotes yet. Paste one above.
1059
- </div>
1060
  )}
1061
  </div>
1062
  </>
@@ -1203,12 +1217,18 @@ export function CalendarWidget({ state, setState, allStates = {} }) {
1203
  const monthLabel = first.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
1204
  const today = new Date().toISOString().slice(0, 10);
1205
 
 
 
 
1206
  return (
1207
  <>
1208
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1209
- <button className="icon-btn" onClick={() => shiftMonth(-1)}><Icon name="chevron" size={14} style={{ transform: 'rotate(180deg)' }}/></button>
1210
  <div style={{ flex: 1, fontWeight: 600, fontSize: 13 }}>{monthLabel}</div>
1211
- <button className="icon-btn" onClick={() => shiftMonth(1)}><Icon name="chevron" size={14}/></button>
 
 
 
1212
  </div>
1213
  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 3, fontSize: 10, color: 'var(--canvas-text-4)', textTransform: 'uppercase', letterSpacing: '0.06em', textAlign: 'center', fontFamily: 'var(--canvas-mono)' }}>
1214
  {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((d, i) => <div key={i}>{d}</div>)}
@@ -1292,9 +1312,7 @@ export function ActivityWidget({ state, setState }) {
1292
  <>
1293
  <div className="note-list">
1294
  {events.length === 0 && (
1295
- <div style={{ padding: 18, textAlign: 'center', color: 'var(--canvas-text-3)', fontSize: 12 }}>
1296
- No activity yet. Edits across widgets will show up here.
1297
- </div>
1298
  )}
1299
  {events.map(e => (
1300
  <div key={e.id} className="note-row" style={{ padding: '7px 9px' }}>
@@ -1413,24 +1431,52 @@ export function DocumenterWidget({ state, setState }) {
1413
  </div>
1414
  ))}
1415
  {entries.length === 0 && (
1416
- <div style={{ padding: 18, textAlign: 'center', color: 'var(--canvas-text-3)', fontSize: 12 }}>
1417
- No entries yet. Drop a quick line about today.
1418
- </div>
1419
  )}
1420
  </div>
1421
  </>
1422
  );
1423
  }
1424
 
1425
- // ===== Stub =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1426
  export function StubWidget({ meta }) {
 
1427
  return (
1428
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8, color: 'var(--canvas-text-3)', textAlign: 'center', padding: '20px 16px' }}>
1429
- <div style={{ width: 38, height: 38, borderRadius: 9, background: 'var(--canvas-surface-2)', display: 'grid', placeItems: 'center', color: 'var(--canvas-text-4)' }}>
1430
- <Icon name={meta.icon} size={18}/>
1431
- </div>
1432
- <div style={{ fontSize: 11, color: 'var(--canvas-text-4)', fontFamily: 'var(--canvas-mono)', textTransform: 'uppercase', letterSpacing: '0.08em' }}>Coming soon</div>
1433
- <div style={{ fontSize: 11.5, color: 'var(--canvas-text-3)', maxWidth: 220 }}>{meta.desc}</div>
 
 
 
 
1434
  </div>
1435
  );
1436
  }
 
8
  const fireActivity = (source, msg) =>
9
  window.dispatchEvent(new CustomEvent('canvas-activity', { detail: { source, msg } }));
10
 
11
+ // Shared empty-state block — every widget uses this so the look stays consistent.
12
+ function EmptyState({ icon = 'sparkles', title, hint, action }) {
13
+ return (
14
+ <div className="widget-empty">
15
+ <div className="widget-empty-icon"><Icon name={icon} size={20}/></div>
16
+ <div className="widget-empty-title">{title}</div>
17
+ {hint && <div className="widget-empty-hint">{hint}</div>}
18
+ {action && <div className="widget-empty-action">{action}</div>}
19
+ </div>
20
+ );
21
+ }
22
+
23
  // Cross-widget drag-drop helpers — one mime type, JSON payload tagged by `kind`.
24
  const X_MIME = 'application/x-canvas-item';
25
  const setDragPayload = (e, kind, payload) => {
 
154
  </div>
155
  ))}
156
  {sorted.length === 0 && (
157
+ <EmptyState
158
+ icon="book"
159
+ title="No citations yet"
160
+ hint="Add one with the + button, paste a DOI, or drag a paper here from the Reading Queue."
161
+ />
162
  )}
163
  </div>
164
  </div>
 
609
  );
610
  })}
611
  {enriched.length === 0 && (
612
+ <EmptyState
613
+ icon="calendar"
614
+ title="No deadlines yet"
615
+ hint="Track due dates and download them straight into your calendar as .ics files."
616
+ />
617
  )}
618
  </div>
619
  <button className="add-tiny" onClick={() => openModal('add-deadline', {
 
769
  </div>
770
  ))}
771
  {filtered.length === 0 && (
772
+ notes.length === 0 ? (
773
+ <EmptyState icon="notes" title="No notes yet" hint="Capture quick thoughts. Markdown supported. Type @ to mention citations, chapters, or tasks."/>
774
+ ) : (
775
+ <EmptyState icon="search" title={`No notes match "${search}"`} hint="Try a shorter query or clear the search box."/>
776
+ )
777
  )}
778
  </div>
779
  <button className="add-tiny" onClick={add}>+ New note</button>
 
836
  );
837
  })}
838
  {habits.length === 0 && (
839
+ <EmptyState icon="flame" title="No habits yet" hint="Track daily research practices. Read 1 paper, write 30 min, lab notebook entry."/>
840
  )}
841
  </div>
842
  <button className="add-tiny" onClick={add}>+ New habit</button>
 
875
  </div>
876
  ))}
877
  {goals.length === 0 && (
878
+ <EmptyState icon="bullseye" title="No goals yet" hint="Quarterly OKRs, dissertation milestones, anything you want to track."/>
879
  )}
880
  </div>
881
  <button className="add-tiny" onClick={add}>+ New goal</button>
 
908
  </div>
909
  ))}
910
  {meetings.length === 0 && (
911
+ <EmptyState icon="message" title="No meetings logged" hint="Capture decisions, action items, and last contact for each stakeholder."/>
 
 
912
  )}
913
  </div>
914
  <button className="add-tiny" onClick={add}>+ Log meeting</button>
 
953
  <>
954
  <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
955
  {items.length === 0 ? (
956
+ <EmptyState icon="list" title="Empty outline" hint="Click + Node below. Use Tab/Shift+Tab to indent, Enter for a sibling."/>
 
 
957
  ) : items.map((it, i) => {
958
  const hasChildren = items[i + 1] && items[i + 1].depth > it.depth;
959
  const isCollapsed = !!expanded[it.id] && hasChildren;
 
1070
  </div>
1071
  ))}
1072
  {items.length === 0 && (
1073
+ <EmptyState icon="cite" title="No quotes yet" hint="Paste a quote above with citation key and page; copy formatted with one click."/>
 
 
1074
  )}
1075
  </div>
1076
  </>
 
1217
  const monthLabel = first.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
1218
  const today = new Date().toISOString().slice(0, 10);
1219
 
1220
+ const goToToday = () => setState({ ...state, viewMonth: new Date().toISOString().slice(0, 7) });
1221
+ const isCurrentMonth = monthStr === new Date().toISOString().slice(0, 7);
1222
+
1223
  return (
1224
  <>
1225
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
1226
+ <button className="icon-btn" onClick={() => shiftMonth(-1)} title="Previous month"><Icon name="chevron" size={14} style={{ transform: 'rotate(180deg)' }}/></button>
1227
  <div style={{ flex: 1, fontWeight: 600, fontSize: 13 }}>{monthLabel}</div>
1228
+ {!isCurrentMonth && (
1229
+ <button className="chip" onClick={goToToday} title="Jump to today" style={{ fontSize: 10.5 }}>Today</button>
1230
+ )}
1231
+ <button className="icon-btn" onClick={() => shiftMonth(1)} title="Next month"><Icon name="chevron" size={14}/></button>
1232
  </div>
1233
  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 3, fontSize: 10, color: 'var(--canvas-text-4)', textTransform: 'uppercase', letterSpacing: '0.06em', textAlign: 'center', fontFamily: 'var(--canvas-mono)' }}>
1234
  {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((d, i) => <div key={i}>{d}</div>)}
 
1312
  <>
1313
  <div className="note-list">
1314
  {events.length === 0 && (
1315
+ <EmptyState icon="graph" title="No activity yet" hint="Edits across your widgets will show up here as you work."/>
 
 
1316
  )}
1317
  {events.map(e => (
1318
  <div key={e.id} className="note-row" style={{ padding: '7px 9px' }}>
 
1431
  </div>
1432
  ))}
1433
  {entries.length === 0 && (
1434
+ <EmptyState icon="pencil" title="No entries yet" hint="Drop a line about today. Hit ⌘↵ to log it. Tap Weekly summary anytime."/>
 
 
1435
  )}
1436
  </div>
1437
  </>
1438
  );
1439
  }
1440
 
1441
+ // ===== Stub — roadmap preview card =====
1442
+ // Shows what's coming for this widget type so adding it from the palette
1443
+ // doesn't feel like a dead end.
1444
+ const STUB_PLANS = {
1445
+ 'concept-map': ['Drag papers as nodes', 'Connect by theme', 'Auto-cluster by citation overlap'],
1446
+ 'highlights': ['Pull quotes with auto-citation', 'Search across all notes'],
1447
+ 'paper-tldr': ['Drop a PDF', 'Get claim / method / limits / gaps', 'Save to Bibliography in one click'],
1448
+ 'outline': ['Collapsible tree', 'Drop Insights into slots', 'Promote to Deliverable section'],
1449
+ 'latex': ['Render math as you type', 'Snippet library', 'Copy as image / TeX'],
1450
+ 'draft-locker': ['Versioned chapter drafts', 'Diff between versions', 'Roll back any change'],
1451
+ 'gantt': ['Proposal → IRB → defense timeline', 'Critical-path highlighting', 'Drag to reschedule'],
1452
+ 'mood': ['Daily slider', 'Trend graph', 'Correlate with productive days'],
1453
+ 'sleep': ['Sleep duration vs. word output', 'Energy heatmap', 'Apple Health import'],
1454
+ 'focus': ['Curated ambient playlists', 'Focus session timer', 'Auto-pause on Pomodoro break'],
1455
+ 'cfp': ['CFP deadlines by venue', 'Fit score by topic', 'Submission status pipeline'],
1456
+ 'grants': ['Grant deadlines + amounts', 'Award log', 'Generate budget justification'],
1457
+ 'crm': ['Collaborators with last touch', 'Quick-add from Meeting Log', 'Reminders for cold contacts'],
1458
+ 'cv': ['Track outputs over time', 'Auto-generate CV from Bibliography', 'Highlight by impact factor'],
1459
+ 'datasets': ['Public datasets by domain', 'Saved searches', 'License + access notes'],
1460
+ 'methods': ['When to use what test', 'Examples + citations', 'Saved templates per chapter'],
1461
+ 'discounts': ['Software & services with edu pricing', 'Discount expiration tracking'],
1462
+ 'assumption': ['Names hidden assumptions', 'Asks "what if wrong?"', 'Logs to a hypothesis tree'],
1463
+ 'whats-missing': ['Gap analysis on lit review', 'Compares to top venues', 'Suggests targeted reads'],
1464
+ 'calibrator': ['Challenges every "results show" claim', 'Asks for the prior', 'Flags p-hacking patterns'],
1465
+ };
1466
+
1467
  export function StubWidget({ meta }) {
1468
+ const plan = STUB_PLANS[meta.type] || [];
1469
  return (
1470
+ <div className="widget-stub">
1471
+ <div className="widget-stub-icon"><Icon name={meta.icon} size={18}/></div>
1472
+ <div className="widget-stub-tag">Coming soon</div>
1473
+ <div className="widget-stub-title">{meta.name}</div>
1474
+ <div className="widget-stub-desc">{meta.desc}</div>
1475
+ {plan.length > 0 && (
1476
+ <ul className="widget-stub-plan">
1477
+ {plan.map((p, i) => <li key={i}>{p}</li>)}
1478
+ </ul>
1479
+ )}
1480
  </div>
1481
  );
1482
  }
phd-advisor-frontend/src/pages/CanvasPage.js CHANGED
@@ -432,7 +432,8 @@ const CanvasPage = ({ user, authToken, onNavigateToHome, onNavigateToChat, onSig
432
  openModal('global-search', { states: widgetStates });
433
  }, [openModal, widgetStates]);
434
 
435
- // Esc closes modal, ⌘K opens command palette, ⌘/ opens global content search
 
436
  useEffect(() => {
437
  const k = (e) => {
438
  if (e.key === 'Escape') closeModal();
@@ -444,6 +445,11 @@ const CanvasPage = ({ user, authToken, onNavigateToHome, onNavigateToChat, onSig
444
  e.preventDefault();
445
  openGlobalSearch();
446
  }
 
 
 
 
 
447
  };
448
  window.addEventListener('keydown', k);
449
  return () => window.removeEventListener('keydown', k);
@@ -510,8 +516,30 @@ const CanvasPage = ({ user, authToken, onNavigateToHome, onNavigateToChat, onSig
510
  <ModalRouter modal={modal} onClose={closeModal}/>
511
  <ToastStack/>
512
  <CanvasWelcomeTour key={tourForceShow} forceShow={tourForceShow > 0}/>
 
513
  </div>
514
  );
515
  };
516
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  export default CanvasPage;
 
432
  openModal('global-search', { states: widgetStates });
433
  }, [openModal, widgetStates]);
434
 
435
+ // Esc closes modal, ⌘K opens command palette, ⌘/ opens global content search,
436
+ // ? opens the welcome tour for help (matches the icon in the topbar).
437
  useEffect(() => {
438
  const k = (e) => {
439
  if (e.key === 'Escape') closeModal();
 
445
  e.preventDefault();
446
  openGlobalSearch();
447
  }
448
+ // ? key (Shift+/) — only when the user isn't typing in an input
449
+ if (e.key === '?' && !['INPUT', 'TEXTAREA'].includes(e.target.tagName)) {
450
+ e.preventDefault();
451
+ setTourForceShow(n => n + 1);
452
+ }
453
  };
454
  window.addEventListener('keydown', k);
455
  return () => window.removeEventListener('keydown', k);
 
516
  <ModalRouter modal={modal} onClose={closeModal}/>
517
  <ToastStack/>
518
  <CanvasWelcomeTour key={tourForceShow} forceShow={tourForceShow > 0}/>
519
+ <ShortcutHint/>
520
  </div>
521
  );
522
  };
523
 
524
+ // Subtle floating hint bar showing the most-used keyboard shortcuts.
525
+ // Auto-hides on small screens and after the first 12s, until the user hovers.
526
+ function ShortcutHint() {
527
+ const [visible, setVisible] = useState(true);
528
+ useEffect(() => {
529
+ const t = setTimeout(() => setVisible(false), 12000);
530
+ return () => clearTimeout(t);
531
+ }, []);
532
+ return (
533
+ <div
534
+ className={`canvas-shortcut-hint ${visible ? 'visible' : ''}`}
535
+ onMouseEnter={() => setVisible(true)}
536
+ onMouseLeave={() => setVisible(false)}
537
+ >
538
+ <span><kbd>⌘</kbd><kbd>K</kbd> commands</span>
539
+ <span><kbd>⌘</kbd><kbd>/</kbd> search</span>
540
+ <span><kbd>?</kbd> help</span>
541
+ </div>
542
+ );
543
+ }
544
+
545
  export default CanvasPage;
phd-advisor-frontend/src/styles/CanvasPage.css CHANGED
@@ -88,6 +88,54 @@
88
 
89
  .canvas-page-with-sidebar *, .canvas-modal-backdrop *, .toast-stack * { box-sizing: border-box; }
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  .canvas-page-with-sidebar button {
92
  font-family: inherit;
93
  cursor: pointer;
@@ -192,8 +240,19 @@
192
  top: 50%;
193
  transform: translate(-50%, -50%);
194
  }
 
195
  @media (max-width: 900px) {
196
  .floating-header > .chat-view-tabs { display: none; }
 
 
 
 
 
 
 
 
 
 
197
  }
198
 
199
  /* Shared view-tabs pill bar — used on both Canvas and Chat pages.
@@ -1839,6 +1898,88 @@ body[data-canvas-theme="light"] .canvas-modal-backdrop .critic-meter .bar { back
1839
  background: rgba(0, 0, 0, 0.04);
1840
  }
1841
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1842
  /* Cross-widget drop overlay — shows over the target widget body during drag */
1843
  .canvas-page-with-sidebar .canvas-drop-overlay {
1844
  position: absolute;
@@ -1907,6 +2048,94 @@ body[data-canvas-theme="light"] .canvas-modal-backdrop .critic-meter .bar { back
1907
  outline: none;
1908
  }
1909
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1910
  /* Toast */
1911
  .toast-stack {
1912
  position: fixed;
 
88
 
89
  .canvas-page-with-sidebar *, .canvas-modal-backdrop *, .toast-stack * { box-sizing: border-box; }
90
 
91
+ /* Standardized motion tokens */
92
+ .canvas-page-with-sidebar, .canvas-modal-backdrop {
93
+ --canvas-ease: cubic-bezier(0.2, 0, 0, 1);
94
+ --canvas-fast: 120ms var(--canvas-ease);
95
+ --canvas-base: 180ms var(--canvas-ease);
96
+ --canvas-slow: 280ms var(--canvas-ease);
97
+ }
98
+
99
+ /* Widgets fade + slight rise on mount */
100
+ @keyframes canvas-widget-in {
101
+ from { opacity: 0; transform: translateY(4px); }
102
+ to { opacity: 1; transform: none; }
103
+ }
104
+ .canvas-page-with-sidebar .widget {
105
+ animation: canvas-widget-in 240ms var(--canvas-ease);
106
+ }
107
+
108
+ /* Smooth content view transitions */
109
+ @keyframes canvas-view-in {
110
+ from { opacity: 0; transform: translateY(2px); }
111
+ to { opacity: 1; transform: none; }
112
+ }
113
+ .canvas-content > * {
114
+ animation: canvas-view-in 220ms var(--canvas-ease);
115
+ }
116
+
117
+ /* Improved focus rings — accent halo, no harsh outline */
118
+ .canvas-page-with-sidebar :focus-visible,
119
+ .canvas-modal-backdrop :focus-visible {
120
+ outline: none;
121
+ box-shadow: 0 0 0 3px var(--canvas-accent-glow);
122
+ border-radius: 4px;
123
+ }
124
+ .canvas-page-with-sidebar input:focus-visible,
125
+ .canvas-page-with-sidebar textarea:focus-visible,
126
+ .canvas-modal-backdrop input:focus-visible,
127
+ .canvas-modal-backdrop textarea:focus-visible {
128
+ outline: none;
129
+ border-color: var(--canvas-accent);
130
+ box-shadow: 0 0 0 3px var(--canvas-accent-glow);
131
+ }
132
+
133
+ /* Respect users who don't want motion */
134
+ @media (prefers-reduced-motion: reduce) {
135
+ .canvas-page-with-sidebar .widget,
136
+ .canvas-content > * { animation: none; }
137
+ }
138
+
139
  .canvas-page-with-sidebar button {
140
  font-family: inherit;
141
  cursor: pointer;
 
240
  top: 50%;
241
  transform: translate(-50%, -50%);
242
  }
243
+ .canvas-tabs-mobile { display: none; }
244
  @media (max-width: 900px) {
245
  .floating-header > .chat-view-tabs { display: none; }
246
+ .canvas-tabs-mobile {
247
+ display: inline-block;
248
+ background: var(--canvas-surface, var(--bg-tertiary, #f3f4f6));
249
+ border: 1px solid var(--canvas-border, var(--border-primary, #e5e7eb));
250
+ color: var(--canvas-text, var(--text-primary, #111827));
251
+ font-family: inherit;
252
+ font-size: 13px;
253
+ padding: 6px 10px;
254
+ border-radius: 7px;
255
+ }
256
  }
257
 
258
  /* Shared view-tabs pill bar — used on both Canvas and Chat pages.
 
1898
  background: rgba(0, 0, 0, 0.04);
1899
  }
1900
 
1901
+ /* "Coming soon" stub widget — roadmap-preview card, not a dead end */
1902
+ .canvas-page-with-sidebar .widget-stub {
1903
+ flex: 1;
1904
+ display: flex;
1905
+ flex-direction: column;
1906
+ gap: 6px;
1907
+ padding: 18px 4px 8px;
1908
+ }
1909
+ .canvas-page-with-sidebar .widget-stub-icon {
1910
+ width: 32px;
1911
+ height: 32px;
1912
+ border-radius: 7px;
1913
+ background: var(--canvas-surface-2);
1914
+ color: var(--canvas-text-3);
1915
+ display: grid;
1916
+ place-items: center;
1917
+ }
1918
+ .canvas-page-with-sidebar .widget-stub-tag {
1919
+ font-size: 9.5px;
1920
+ font-family: var(--canvas-mono);
1921
+ text-transform: uppercase;
1922
+ letter-spacing: 0.08em;
1923
+ color: var(--canvas-text-4);
1924
+ font-weight: 600;
1925
+ margin-top: 8px;
1926
+ }
1927
+ .canvas-page-with-sidebar .widget-stub-title {
1928
+ font-size: 14px;
1929
+ font-weight: 600;
1930
+ color: var(--canvas-text);
1931
+ }
1932
+ .canvas-page-with-sidebar .widget-stub-desc {
1933
+ font-size: 12px;
1934
+ color: var(--canvas-text-3);
1935
+ line-height: 1.5;
1936
+ }
1937
+ .canvas-page-with-sidebar .widget-stub-plan {
1938
+ margin: 8px 0 0;
1939
+ padding-left: 18px;
1940
+ font-size: 12px;
1941
+ color: var(--canvas-text-3);
1942
+ line-height: 1.6;
1943
+ }
1944
+ .canvas-page-with-sidebar .widget-stub-plan li::marker { color: var(--canvas-accent); }
1945
+
1946
+ /* Shared empty-state block inside widgets (Notion-style: airy, single CTA) */
1947
+ .canvas-page-with-sidebar .widget-empty {
1948
+ display: flex;
1949
+ flex-direction: column;
1950
+ align-items: center;
1951
+ justify-content: center;
1952
+ gap: 6px;
1953
+ padding: 28px 16px;
1954
+ text-align: center;
1955
+ color: var(--canvas-text-3);
1956
+ flex: 1;
1957
+ }
1958
+ .canvas-page-with-sidebar .widget-empty-icon {
1959
+ width: 36px;
1960
+ height: 36px;
1961
+ border-radius: 8px;
1962
+ background: var(--canvas-surface-2);
1963
+ color: var(--canvas-text-3);
1964
+ display: grid;
1965
+ place-items: center;
1966
+ margin-bottom: 4px;
1967
+ }
1968
+ .canvas-page-with-sidebar .widget-empty-title {
1969
+ font-size: 13.5px;
1970
+ font-weight: 600;
1971
+ color: var(--canvas-text-2);
1972
+ }
1973
+ .canvas-page-with-sidebar .widget-empty-hint {
1974
+ font-size: 12px;
1975
+ color: var(--canvas-text-3);
1976
+ max-width: 240px;
1977
+ line-height: 1.5;
1978
+ }
1979
+ .canvas-page-with-sidebar .widget-empty-action {
1980
+ margin-top: 8px;
1981
+ }
1982
+
1983
  /* Cross-widget drop overlay — shows over the target widget body during drag */
1984
  .canvas-page-with-sidebar .canvas-drop-overlay {
1985
  position: absolute;
 
2048
  outline: none;
2049
  }
2050
 
2051
+ /* Print: just the Notion page itself, no chrome */
2052
+ @media print {
2053
+ .canvas-sidebar,
2054
+ .canvas-page-with-sidebar .floating-header,
2055
+ .canvas-page-with-sidebar .canvas-tabs,
2056
+ .canvas-page-with-sidebar .notion-toc,
2057
+ .canvas-page-with-sidebar .deliverable-insertables,
2058
+ .canvas-page-with-sidebar .canvas-shortcut-hint,
2059
+ .canvas-page-with-sidebar .page-header > div:last-child,
2060
+ .toast-stack,
2061
+ .canvas-modal-backdrop {
2062
+ display: none !important;
2063
+ }
2064
+ .canvas-page-with-sidebar,
2065
+ .canvas-main-area,
2066
+ .canvas-app-shell,
2067
+ .canvas-content,
2068
+ .canvas-page-with-sidebar .notion-deliverable-grid,
2069
+ .canvas-page-with-sidebar .notion-page-wrap {
2070
+ display: block !important;
2071
+ overflow: visible !important;
2072
+ margin: 0 !important;
2073
+ padding: 0 !important;
2074
+ background: #ffffff !important;
2075
+ color: #000 !important;
2076
+ }
2077
+ .canvas-page-with-sidebar .notion-page {
2078
+ background: #ffffff !important;
2079
+ border: none !important;
2080
+ box-shadow: none !important;
2081
+ padding: 0 !important;
2082
+ max-width: none !important;
2083
+ }
2084
+ .canvas-page-with-sidebar .notion-page-title { font-size: 28px; color: #000 !important; }
2085
+ .canvas-page-with-sidebar .notion-h2 { color: #000 !important; }
2086
+ .canvas-page-with-sidebar .notion-text { color: #000 !important; padding: 0 !important; margin: 0 !important; }
2087
+ .canvas-page-with-sidebar .check-pill,
2088
+ .canvas-page-with-sidebar .notion-callout { display: none !important; }
2089
+ }
2090
+
2091
+ /* Floating shortcut hint — subtle, hides itself, hover-revealed */
2092
+ .canvas-shortcut-hint {
2093
+ position: fixed;
2094
+ bottom: 16px;
2095
+ right: 20px;
2096
+ z-index: 50;
2097
+ display: flex;
2098
+ gap: 14px;
2099
+ padding: 8px 14px;
2100
+ border-radius: 999px;
2101
+ background: var(--canvas-surface);
2102
+ border: 1px solid var(--canvas-border);
2103
+ box-shadow: var(--canvas-shadow);
2104
+ font-size: 11.5px;
2105
+ color: var(--canvas-text-3);
2106
+ font-family: 'Inter', sans-serif;
2107
+ opacity: 0;
2108
+ transform: translateY(8px);
2109
+ transition: opacity 0.25s var(--canvas-ease), transform 0.25s var(--canvas-ease);
2110
+ pointer-events: none;
2111
+ }
2112
+ .canvas-shortcut-hint.visible {
2113
+ opacity: 1;
2114
+ transform: none;
2115
+ pointer-events: auto;
2116
+ }
2117
+ .canvas-shortcut-hint:not(.visible):hover {
2118
+ opacity: 0.6;
2119
+ transform: none;
2120
+ pointer-events: auto;
2121
+ }
2122
+ .canvas-shortcut-hint kbd {
2123
+ display: inline-block;
2124
+ font-family: var(--canvas-mono);
2125
+ font-size: 10px;
2126
+ padding: 1px 5px;
2127
+ border-radius: 3px;
2128
+ background: var(--canvas-surface-2);
2129
+ border: 1px solid var(--canvas-border-2);
2130
+ color: var(--canvas-text-2);
2131
+ margin-right: 2px;
2132
+ min-width: 14px;
2133
+ text-align: center;
2134
+ }
2135
+ @media (max-width: 768px) {
2136
+ .canvas-shortcut-hint { display: none; }
2137
+ }
2138
+
2139
  /* Toast */
2140
  .toast-stack {
2141
  position: fixed;