Spaces:
Sleeping
Sleeping
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 +28 -6
- phd-advisor-frontend/src/components/canvas/CanvasDeliverables.js +1 -0
- phd-advisor-frontend/src/components/canvas/CanvasWidgets.js +80 -34
- phd-advisor-frontend/src/pages/CanvasPage.js +29 -1
- phd-advisor-frontend/src/styles/CanvasPage.css +229 -0
phd-advisor-frontend/src/components/AppHeader.js
CHANGED
|
@@ -64,12 +64,34 @@ const AppHeader = ({
|
|
| 64 |
</div>
|
| 65 |
</div>
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
<
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
| 148 |
)}
|
| 149 |
</div>
|
| 150 |
</div>
|
|
@@ -595,7 +609,11 @@ export function DeadlinesWidget({ state, setState, openModal }) {
|
|
| 595 |
);
|
| 596 |
})}
|
| 597 |
{enriched.length === 0 && (
|
| 598 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 755 |
-
|
| 756 |
-
|
|
|
|
|
|
|
| 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 |
-
<
|
| 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 |
-
<
|
| 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 |
-
<
|
| 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 |
-
<
|
| 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 |
-
<
|
| 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:
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
| 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 |
-
<
|
| 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
|
| 1429 |
-
<div
|
| 1430 |
-
|
| 1431 |
-
</div>
|
| 1432 |
-
<div
|
| 1433 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|