Spaces:
Sleeping
Sleeping
Neon:ryan commited on
Commit ·
dd07b04
1
Parent(s): 7f1718b
Enhance deliverables functionality and UI
Browse files- Added support for multiple project drafts in the deliverables view, allowing users to manage and switch between various templates (e.g., thesis chapters, CVs, cover letters).
- Integrated LaTeX rendering capabilities using remark-math and rehype-katex for improved mathematical expression handling.
- Updated CSS styles for deliverable project tabs and rich editor blocks to enhance user experience and visual consistency.
This update significantly improves the usability of the deliverables feature, making it more versatile for users managing multiple projects.
phd-advisor-frontend/package-lock.json
CHANGED
|
@@ -12,12 +12,15 @@
|
|
| 12 |
"@testing-library/jest-dom": "^6.6.3",
|
| 13 |
"@testing-library/react": "^16.3.0",
|
| 14 |
"@testing-library/user-event": "^13.5.0",
|
|
|
|
| 15 |
"lucide-react": "^0.544.0",
|
| 16 |
"react": "^19.1.0",
|
| 17 |
"react-dom": "^19.1.0",
|
| 18 |
"react-markdown": "^10.1.0",
|
| 19 |
"react-scripts": "5.0.1",
|
|
|
|
| 20 |
"remark-gfm": "^4.0.1",
|
|
|
|
| 21 |
"web-vitals": "^2.1.4"
|
| 22 |
}
|
| 23 |
},
|
|
@@ -3797,6 +3800,12 @@
|
|
| 3797 |
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
| 3798 |
"license": "MIT"
|
| 3799 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3800 |
"node_modules/@types/mdast": {
|
| 3801 |
"version": "4.0.4",
|
| 3802 |
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
|
@@ -3866,16 +3875,6 @@
|
|
| 3866 |
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
| 3867 |
"license": "MIT"
|
| 3868 |
},
|
| 3869 |
-
"node_modules/@types/react": {
|
| 3870 |
-
"version": "19.1.9",
|
| 3871 |
-
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
|
| 3872 |
-
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
|
| 3873 |
-
"license": "MIT",
|
| 3874 |
-
"peer": true,
|
| 3875 |
-
"dependencies": {
|
| 3876 |
-
"csstype": "^3.0.2"
|
| 3877 |
-
}
|
| 3878 |
-
},
|
| 3879 |
"node_modules/@types/resolve": {
|
| 3880 |
"version": "1.17.1",
|
| 3881 |
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
|
@@ -6486,13 +6485,6 @@
|
|
| 6486 |
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
|
| 6487 |
"license": "MIT"
|
| 6488 |
},
|
| 6489 |
-
"node_modules/csstype": {
|
| 6490 |
-
"version": "3.1.3",
|
| 6491 |
-
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
| 6492 |
-
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
| 6493 |
-
"license": "MIT",
|
| 6494 |
-
"peer": true
|
| 6495 |
-
},
|
| 6496 |
"node_modules/damerau-levenshtein": {
|
| 6497 |
"version": "1.0.8",
|
| 6498 |
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
|
@@ -9017,6 +9009,125 @@
|
|
| 9017 |
"node": ">= 0.4"
|
| 9018 |
}
|
| 9019 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9020 |
"node_modules/hast-util-to-jsx-runtime": {
|
| 9021 |
"version": "2.3.6",
|
| 9022 |
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
|
@@ -9044,6 +9155,22 @@
|
|
| 9044 |
"url": "https://opencollective.com/unified"
|
| 9045 |
}
|
| 9046 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9047 |
"node_modules/hast-util-whitespace": {
|
| 9048 |
"version": "3.0.0",
|
| 9049 |
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
|
@@ -9057,6 +9184,23 @@
|
|
| 9057 |
"url": "https://opencollective.com/unified"
|
| 9058 |
}
|
| 9059 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9060 |
"node_modules/he": {
|
| 9061 |
"version": "1.2.0",
|
| 9062 |
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
|
@@ -11312,6 +11456,22 @@
|
|
| 11312 |
"node": ">=4.0"
|
| 11313 |
}
|
| 11314 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11315 |
"node_modules/keyv": {
|
| 11316 |
"version": "4.5.4",
|
| 11317 |
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
|
@@ -11756,6 +11916,25 @@
|
|
| 11756 |
"url": "https://opencollective.com/unified"
|
| 11757 |
}
|
| 11758 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11759 |
"node_modules/mdast-util-mdx-expression": {
|
| 11760 |
"version": "2.0.1",
|
| 11761 |
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
|
|
@@ -12135,6 +12314,25 @@
|
|
| 12135 |
"url": "https://opencollective.com/unified"
|
| 12136 |
}
|
| 12137 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12138 |
"node_modules/micromark-factory-destination": {
|
| 12139 |
"version": "2.0.1",
|
| 12140 |
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
|
|
@@ -15451,6 +15649,25 @@
|
|
| 15451 |
"node": ">=6"
|
| 15452 |
}
|
| 15453 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15454 |
"node_modules/relateurl": {
|
| 15455 |
"version": "0.2.7",
|
| 15456 |
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
|
|
@@ -15478,6 +15695,22 @@
|
|
| 15478 |
"url": "https://opencollective.com/unified"
|
| 15479 |
}
|
| 15480 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15481 |
"node_modules/remark-parse": {
|
| 15482 |
"version": "11.0.0",
|
| 15483 |
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
|
|
@@ -17704,20 +17937,6 @@
|
|
| 17704 |
"is-typedarray": "^1.0.0"
|
| 17705 |
}
|
| 17706 |
},
|
| 17707 |
-
"node_modules/typescript": {
|
| 17708 |
-
"version": "4.9.5",
|
| 17709 |
-
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
| 17710 |
-
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
| 17711 |
-
"license": "Apache-2.0",
|
| 17712 |
-
"peer": true,
|
| 17713 |
-
"bin": {
|
| 17714 |
-
"tsc": "bin/tsc",
|
| 17715 |
-
"tsserver": "bin/tsserver"
|
| 17716 |
-
},
|
| 17717 |
-
"engines": {
|
| 17718 |
-
"node": ">=4.2.0"
|
| 17719 |
-
}
|
| 17720 |
-
},
|
| 17721 |
"node_modules/unbox-primitive": {
|
| 17722 |
"version": "1.1.0",
|
| 17723 |
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
|
@@ -17831,6 +18050,20 @@
|
|
| 17831 |
"node": ">=8"
|
| 17832 |
}
|
| 17833 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17834 |
"node_modules/unist-util-is": {
|
| 17835 |
"version": "6.0.0",
|
| 17836 |
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
|
|
@@ -17857,6 +18090,20 @@
|
|
| 17857 |
"url": "https://opencollective.com/unified"
|
| 17858 |
}
|
| 17859 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17860 |
"node_modules/unist-util-stringify-position": {
|
| 17861 |
"version": "4.0.0",
|
| 17862 |
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
|
@@ -18070,6 +18317,20 @@
|
|
| 18070 |
"url": "https://opencollective.com/unified"
|
| 18071 |
}
|
| 18072 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18073 |
"node_modules/vfile-message": {
|
| 18074 |
"version": "4.0.3",
|
| 18075 |
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
|
|
@@ -18137,6 +18398,16 @@
|
|
| 18137 |
"minimalistic-assert": "^1.0.0"
|
| 18138 |
}
|
| 18139 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18140 |
"node_modules/web-vitals": {
|
| 18141 |
"version": "2.1.4",
|
| 18142 |
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz",
|
|
|
|
| 12 |
"@testing-library/jest-dom": "^6.6.3",
|
| 13 |
"@testing-library/react": "^16.3.0",
|
| 14 |
"@testing-library/user-event": "^13.5.0",
|
| 15 |
+
"katex": "^0.16.45",
|
| 16 |
"lucide-react": "^0.544.0",
|
| 17 |
"react": "^19.1.0",
|
| 18 |
"react-dom": "^19.1.0",
|
| 19 |
"react-markdown": "^10.1.0",
|
| 20 |
"react-scripts": "5.0.1",
|
| 21 |
+
"rehype-katex": "^7.0.1",
|
| 22 |
"remark-gfm": "^4.0.1",
|
| 23 |
+
"remark-math": "^6.0.0",
|
| 24 |
"web-vitals": "^2.1.4"
|
| 25 |
}
|
| 26 |
},
|
|
|
|
| 3800 |
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
| 3801 |
"license": "MIT"
|
| 3802 |
},
|
| 3803 |
+
"node_modules/@types/katex": {
|
| 3804 |
+
"version": "0.16.8",
|
| 3805 |
+
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz",
|
| 3806 |
+
"integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==",
|
| 3807 |
+
"license": "MIT"
|
| 3808 |
+
},
|
| 3809 |
"node_modules/@types/mdast": {
|
| 3810 |
"version": "4.0.4",
|
| 3811 |
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
|
|
|
| 3875 |
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
| 3876 |
"license": "MIT"
|
| 3877 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3878 |
"node_modules/@types/resolve": {
|
| 3879 |
"version": "1.17.1",
|
| 3880 |
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
|
|
|
| 6485 |
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
|
| 6486 |
"license": "MIT"
|
| 6487 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6488 |
"node_modules/damerau-levenshtein": {
|
| 6489 |
"version": "1.0.8",
|
| 6490 |
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
|
|
|
| 9009 |
"node": ">= 0.4"
|
| 9010 |
}
|
| 9011 |
},
|
| 9012 |
+
"node_modules/hast-util-from-dom": {
|
| 9013 |
+
"version": "5.0.1",
|
| 9014 |
+
"resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz",
|
| 9015 |
+
"integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==",
|
| 9016 |
+
"license": "ISC",
|
| 9017 |
+
"dependencies": {
|
| 9018 |
+
"@types/hast": "^3.0.0",
|
| 9019 |
+
"hastscript": "^9.0.0",
|
| 9020 |
+
"web-namespaces": "^2.0.0"
|
| 9021 |
+
},
|
| 9022 |
+
"funding": {
|
| 9023 |
+
"type": "opencollective",
|
| 9024 |
+
"url": "https://opencollective.com/unified"
|
| 9025 |
+
}
|
| 9026 |
+
},
|
| 9027 |
+
"node_modules/hast-util-from-html": {
|
| 9028 |
+
"version": "2.0.3",
|
| 9029 |
+
"resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
|
| 9030 |
+
"integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==",
|
| 9031 |
+
"license": "MIT",
|
| 9032 |
+
"dependencies": {
|
| 9033 |
+
"@types/hast": "^3.0.0",
|
| 9034 |
+
"devlop": "^1.1.0",
|
| 9035 |
+
"hast-util-from-parse5": "^8.0.0",
|
| 9036 |
+
"parse5": "^7.0.0",
|
| 9037 |
+
"vfile": "^6.0.0",
|
| 9038 |
+
"vfile-message": "^4.0.0"
|
| 9039 |
+
},
|
| 9040 |
+
"funding": {
|
| 9041 |
+
"type": "opencollective",
|
| 9042 |
+
"url": "https://opencollective.com/unified"
|
| 9043 |
+
}
|
| 9044 |
+
},
|
| 9045 |
+
"node_modules/hast-util-from-html-isomorphic": {
|
| 9046 |
+
"version": "2.0.0",
|
| 9047 |
+
"resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz",
|
| 9048 |
+
"integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==",
|
| 9049 |
+
"license": "MIT",
|
| 9050 |
+
"dependencies": {
|
| 9051 |
+
"@types/hast": "^3.0.0",
|
| 9052 |
+
"hast-util-from-dom": "^5.0.0",
|
| 9053 |
+
"hast-util-from-html": "^2.0.0",
|
| 9054 |
+
"unist-util-remove-position": "^5.0.0"
|
| 9055 |
+
},
|
| 9056 |
+
"funding": {
|
| 9057 |
+
"type": "opencollective",
|
| 9058 |
+
"url": "https://opencollective.com/unified"
|
| 9059 |
+
}
|
| 9060 |
+
},
|
| 9061 |
+
"node_modules/hast-util-from-html/node_modules/entities": {
|
| 9062 |
+
"version": "6.0.1",
|
| 9063 |
+
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
| 9064 |
+
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
| 9065 |
+
"license": "BSD-2-Clause",
|
| 9066 |
+
"engines": {
|
| 9067 |
+
"node": ">=0.12"
|
| 9068 |
+
},
|
| 9069 |
+
"funding": {
|
| 9070 |
+
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 9071 |
+
}
|
| 9072 |
+
},
|
| 9073 |
+
"node_modules/hast-util-from-html/node_modules/parse5": {
|
| 9074 |
+
"version": "7.3.0",
|
| 9075 |
+
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
| 9076 |
+
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
| 9077 |
+
"license": "MIT",
|
| 9078 |
+
"dependencies": {
|
| 9079 |
+
"entities": "^6.0.0"
|
| 9080 |
+
},
|
| 9081 |
+
"funding": {
|
| 9082 |
+
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
| 9083 |
+
}
|
| 9084 |
+
},
|
| 9085 |
+
"node_modules/hast-util-from-parse5": {
|
| 9086 |
+
"version": "8.0.3",
|
| 9087 |
+
"resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
|
| 9088 |
+
"integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==",
|
| 9089 |
+
"license": "MIT",
|
| 9090 |
+
"dependencies": {
|
| 9091 |
+
"@types/hast": "^3.0.0",
|
| 9092 |
+
"@types/unist": "^3.0.0",
|
| 9093 |
+
"devlop": "^1.0.0",
|
| 9094 |
+
"hastscript": "^9.0.0",
|
| 9095 |
+
"property-information": "^7.0.0",
|
| 9096 |
+
"vfile": "^6.0.0",
|
| 9097 |
+
"vfile-location": "^5.0.0",
|
| 9098 |
+
"web-namespaces": "^2.0.0"
|
| 9099 |
+
},
|
| 9100 |
+
"funding": {
|
| 9101 |
+
"type": "opencollective",
|
| 9102 |
+
"url": "https://opencollective.com/unified"
|
| 9103 |
+
}
|
| 9104 |
+
},
|
| 9105 |
+
"node_modules/hast-util-is-element": {
|
| 9106 |
+
"version": "3.0.0",
|
| 9107 |
+
"resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
|
| 9108 |
+
"integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==",
|
| 9109 |
+
"license": "MIT",
|
| 9110 |
+
"dependencies": {
|
| 9111 |
+
"@types/hast": "^3.0.0"
|
| 9112 |
+
},
|
| 9113 |
+
"funding": {
|
| 9114 |
+
"type": "opencollective",
|
| 9115 |
+
"url": "https://opencollective.com/unified"
|
| 9116 |
+
}
|
| 9117 |
+
},
|
| 9118 |
+
"node_modules/hast-util-parse-selector": {
|
| 9119 |
+
"version": "4.0.0",
|
| 9120 |
+
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
|
| 9121 |
+
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
|
| 9122 |
+
"license": "MIT",
|
| 9123 |
+
"dependencies": {
|
| 9124 |
+
"@types/hast": "^3.0.0"
|
| 9125 |
+
},
|
| 9126 |
+
"funding": {
|
| 9127 |
+
"type": "opencollective",
|
| 9128 |
+
"url": "https://opencollective.com/unified"
|
| 9129 |
+
}
|
| 9130 |
+
},
|
| 9131 |
"node_modules/hast-util-to-jsx-runtime": {
|
| 9132 |
"version": "2.3.6",
|
| 9133 |
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
|
|
|
| 9155 |
"url": "https://opencollective.com/unified"
|
| 9156 |
}
|
| 9157 |
},
|
| 9158 |
+
"node_modules/hast-util-to-text": {
|
| 9159 |
+
"version": "4.0.2",
|
| 9160 |
+
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
|
| 9161 |
+
"integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==",
|
| 9162 |
+
"license": "MIT",
|
| 9163 |
+
"dependencies": {
|
| 9164 |
+
"@types/hast": "^3.0.0",
|
| 9165 |
+
"@types/unist": "^3.0.0",
|
| 9166 |
+
"hast-util-is-element": "^3.0.0",
|
| 9167 |
+
"unist-util-find-after": "^5.0.0"
|
| 9168 |
+
},
|
| 9169 |
+
"funding": {
|
| 9170 |
+
"type": "opencollective",
|
| 9171 |
+
"url": "https://opencollective.com/unified"
|
| 9172 |
+
}
|
| 9173 |
+
},
|
| 9174 |
"node_modules/hast-util-whitespace": {
|
| 9175 |
"version": "3.0.0",
|
| 9176 |
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
|
|
|
| 9184 |
"url": "https://opencollective.com/unified"
|
| 9185 |
}
|
| 9186 |
},
|
| 9187 |
+
"node_modules/hastscript": {
|
| 9188 |
+
"version": "9.0.1",
|
| 9189 |
+
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
|
| 9190 |
+
"integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
|
| 9191 |
+
"license": "MIT",
|
| 9192 |
+
"dependencies": {
|
| 9193 |
+
"@types/hast": "^3.0.0",
|
| 9194 |
+
"comma-separated-tokens": "^2.0.0",
|
| 9195 |
+
"hast-util-parse-selector": "^4.0.0",
|
| 9196 |
+
"property-information": "^7.0.0",
|
| 9197 |
+
"space-separated-tokens": "^2.0.0"
|
| 9198 |
+
},
|
| 9199 |
+
"funding": {
|
| 9200 |
+
"type": "opencollective",
|
| 9201 |
+
"url": "https://opencollective.com/unified"
|
| 9202 |
+
}
|
| 9203 |
+
},
|
| 9204 |
"node_modules/he": {
|
| 9205 |
"version": "1.2.0",
|
| 9206 |
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
|
|
|
| 11456 |
"node": ">=4.0"
|
| 11457 |
}
|
| 11458 |
},
|
| 11459 |
+
"node_modules/katex": {
|
| 11460 |
+
"version": "0.16.45",
|
| 11461 |
+
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz",
|
| 11462 |
+
"integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==",
|
| 11463 |
+
"funding": [
|
| 11464 |
+
"https://opencollective.com/katex",
|
| 11465 |
+
"https://github.com/sponsors/katex"
|
| 11466 |
+
],
|
| 11467 |
+
"license": "MIT",
|
| 11468 |
+
"dependencies": {
|
| 11469 |
+
"commander": "^8.3.0"
|
| 11470 |
+
},
|
| 11471 |
+
"bin": {
|
| 11472 |
+
"katex": "cli.js"
|
| 11473 |
+
}
|
| 11474 |
+
},
|
| 11475 |
"node_modules/keyv": {
|
| 11476 |
"version": "4.5.4",
|
| 11477 |
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
|
|
|
| 11916 |
"url": "https://opencollective.com/unified"
|
| 11917 |
}
|
| 11918 |
},
|
| 11919 |
+
"node_modules/mdast-util-math": {
|
| 11920 |
+
"version": "3.0.0",
|
| 11921 |
+
"resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz",
|
| 11922 |
+
"integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==",
|
| 11923 |
+
"license": "MIT",
|
| 11924 |
+
"dependencies": {
|
| 11925 |
+
"@types/hast": "^3.0.0",
|
| 11926 |
+
"@types/mdast": "^4.0.0",
|
| 11927 |
+
"devlop": "^1.0.0",
|
| 11928 |
+
"longest-streak": "^3.0.0",
|
| 11929 |
+
"mdast-util-from-markdown": "^2.0.0",
|
| 11930 |
+
"mdast-util-to-markdown": "^2.1.0",
|
| 11931 |
+
"unist-util-remove-position": "^5.0.0"
|
| 11932 |
+
},
|
| 11933 |
+
"funding": {
|
| 11934 |
+
"type": "opencollective",
|
| 11935 |
+
"url": "https://opencollective.com/unified"
|
| 11936 |
+
}
|
| 11937 |
+
},
|
| 11938 |
"node_modules/mdast-util-mdx-expression": {
|
| 11939 |
"version": "2.0.1",
|
| 11940 |
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
|
|
|
|
| 12314 |
"url": "https://opencollective.com/unified"
|
| 12315 |
}
|
| 12316 |
},
|
| 12317 |
+
"node_modules/micromark-extension-math": {
|
| 12318 |
+
"version": "3.1.0",
|
| 12319 |
+
"resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz",
|
| 12320 |
+
"integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==",
|
| 12321 |
+
"license": "MIT",
|
| 12322 |
+
"dependencies": {
|
| 12323 |
+
"@types/katex": "^0.16.0",
|
| 12324 |
+
"devlop": "^1.0.0",
|
| 12325 |
+
"katex": "^0.16.0",
|
| 12326 |
+
"micromark-factory-space": "^2.0.0",
|
| 12327 |
+
"micromark-util-character": "^2.0.0",
|
| 12328 |
+
"micromark-util-symbol": "^2.0.0",
|
| 12329 |
+
"micromark-util-types": "^2.0.0"
|
| 12330 |
+
},
|
| 12331 |
+
"funding": {
|
| 12332 |
+
"type": "opencollective",
|
| 12333 |
+
"url": "https://opencollective.com/unified"
|
| 12334 |
+
}
|
| 12335 |
+
},
|
| 12336 |
"node_modules/micromark-factory-destination": {
|
| 12337 |
"version": "2.0.1",
|
| 12338 |
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
|
|
|
|
| 15649 |
"node": ">=6"
|
| 15650 |
}
|
| 15651 |
},
|
| 15652 |
+
"node_modules/rehype-katex": {
|
| 15653 |
+
"version": "7.0.1",
|
| 15654 |
+
"resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz",
|
| 15655 |
+
"integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==",
|
| 15656 |
+
"license": "MIT",
|
| 15657 |
+
"dependencies": {
|
| 15658 |
+
"@types/hast": "^3.0.0",
|
| 15659 |
+
"@types/katex": "^0.16.0",
|
| 15660 |
+
"hast-util-from-html-isomorphic": "^2.0.0",
|
| 15661 |
+
"hast-util-to-text": "^4.0.0",
|
| 15662 |
+
"katex": "^0.16.0",
|
| 15663 |
+
"unist-util-visit-parents": "^6.0.0",
|
| 15664 |
+
"vfile": "^6.0.0"
|
| 15665 |
+
},
|
| 15666 |
+
"funding": {
|
| 15667 |
+
"type": "opencollective",
|
| 15668 |
+
"url": "https://opencollective.com/unified"
|
| 15669 |
+
}
|
| 15670 |
+
},
|
| 15671 |
"node_modules/relateurl": {
|
| 15672 |
"version": "0.2.7",
|
| 15673 |
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
|
|
|
|
| 15695 |
"url": "https://opencollective.com/unified"
|
| 15696 |
}
|
| 15697 |
},
|
| 15698 |
+
"node_modules/remark-math": {
|
| 15699 |
+
"version": "6.0.0",
|
| 15700 |
+
"resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz",
|
| 15701 |
+
"integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==",
|
| 15702 |
+
"license": "MIT",
|
| 15703 |
+
"dependencies": {
|
| 15704 |
+
"@types/mdast": "^4.0.0",
|
| 15705 |
+
"mdast-util-math": "^3.0.0",
|
| 15706 |
+
"micromark-extension-math": "^3.0.0",
|
| 15707 |
+
"unified": "^11.0.0"
|
| 15708 |
+
},
|
| 15709 |
+
"funding": {
|
| 15710 |
+
"type": "opencollective",
|
| 15711 |
+
"url": "https://opencollective.com/unified"
|
| 15712 |
+
}
|
| 15713 |
+
},
|
| 15714 |
"node_modules/remark-parse": {
|
| 15715 |
"version": "11.0.0",
|
| 15716 |
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
|
|
|
|
| 17937 |
"is-typedarray": "^1.0.0"
|
| 17938 |
}
|
| 17939 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17940 |
"node_modules/unbox-primitive": {
|
| 17941 |
"version": "1.1.0",
|
| 17942 |
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
|
|
|
| 18050 |
"node": ">=8"
|
| 18051 |
}
|
| 18052 |
},
|
| 18053 |
+
"node_modules/unist-util-find-after": {
|
| 18054 |
+
"version": "5.0.0",
|
| 18055 |
+
"resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
|
| 18056 |
+
"integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==",
|
| 18057 |
+
"license": "MIT",
|
| 18058 |
+
"dependencies": {
|
| 18059 |
+
"@types/unist": "^3.0.0",
|
| 18060 |
+
"unist-util-is": "^6.0.0"
|
| 18061 |
+
},
|
| 18062 |
+
"funding": {
|
| 18063 |
+
"type": "opencollective",
|
| 18064 |
+
"url": "https://opencollective.com/unified"
|
| 18065 |
+
}
|
| 18066 |
+
},
|
| 18067 |
"node_modules/unist-util-is": {
|
| 18068 |
"version": "6.0.0",
|
| 18069 |
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
|
|
|
|
| 18090 |
"url": "https://opencollective.com/unified"
|
| 18091 |
}
|
| 18092 |
},
|
| 18093 |
+
"node_modules/unist-util-remove-position": {
|
| 18094 |
+
"version": "5.0.0",
|
| 18095 |
+
"resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz",
|
| 18096 |
+
"integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==",
|
| 18097 |
+
"license": "MIT",
|
| 18098 |
+
"dependencies": {
|
| 18099 |
+
"@types/unist": "^3.0.0",
|
| 18100 |
+
"unist-util-visit": "^5.0.0"
|
| 18101 |
+
},
|
| 18102 |
+
"funding": {
|
| 18103 |
+
"type": "opencollective",
|
| 18104 |
+
"url": "https://opencollective.com/unified"
|
| 18105 |
+
}
|
| 18106 |
+
},
|
| 18107 |
"node_modules/unist-util-stringify-position": {
|
| 18108 |
"version": "4.0.0",
|
| 18109 |
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
|
|
|
| 18317 |
"url": "https://opencollective.com/unified"
|
| 18318 |
}
|
| 18319 |
},
|
| 18320 |
+
"node_modules/vfile-location": {
|
| 18321 |
+
"version": "5.0.3",
|
| 18322 |
+
"resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
|
| 18323 |
+
"integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
|
| 18324 |
+
"license": "MIT",
|
| 18325 |
+
"dependencies": {
|
| 18326 |
+
"@types/unist": "^3.0.0",
|
| 18327 |
+
"vfile": "^6.0.0"
|
| 18328 |
+
},
|
| 18329 |
+
"funding": {
|
| 18330 |
+
"type": "opencollective",
|
| 18331 |
+
"url": "https://opencollective.com/unified"
|
| 18332 |
+
}
|
| 18333 |
+
},
|
| 18334 |
"node_modules/vfile-message": {
|
| 18335 |
"version": "4.0.3",
|
| 18336 |
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
|
|
|
|
| 18398 |
"minimalistic-assert": "^1.0.0"
|
| 18399 |
}
|
| 18400 |
},
|
| 18401 |
+
"node_modules/web-namespaces": {
|
| 18402 |
+
"version": "2.0.1",
|
| 18403 |
+
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
|
| 18404 |
+
"integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
|
| 18405 |
+
"license": "MIT",
|
| 18406 |
+
"funding": {
|
| 18407 |
+
"type": "github",
|
| 18408 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 18409 |
+
}
|
| 18410 |
+
},
|
| 18411 |
"node_modules/web-vitals": {
|
| 18412 |
"version": "2.1.4",
|
| 18413 |
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz",
|
phd-advisor-frontend/package.json
CHANGED
|
@@ -7,12 +7,15 @@
|
|
| 7 |
"@testing-library/jest-dom": "^6.6.3",
|
| 8 |
"@testing-library/react": "^16.3.0",
|
| 9 |
"@testing-library/user-event": "^13.5.0",
|
|
|
|
| 10 |
"lucide-react": "^0.544.0",
|
| 11 |
"react": "^19.1.0",
|
| 12 |
"react-dom": "^19.1.0",
|
| 13 |
"react-markdown": "^10.1.0",
|
| 14 |
"react-scripts": "5.0.1",
|
|
|
|
| 15 |
"remark-gfm": "^4.0.1",
|
|
|
|
| 16 |
"web-vitals": "^2.1.4"
|
| 17 |
},
|
| 18 |
"scripts": {
|
|
|
|
| 7 |
"@testing-library/jest-dom": "^6.6.3",
|
| 8 |
"@testing-library/react": "^16.3.0",
|
| 9 |
"@testing-library/user-event": "^13.5.0",
|
| 10 |
+
"katex": "^0.16.45",
|
| 11 |
"lucide-react": "^0.544.0",
|
| 12 |
"react": "^19.1.0",
|
| 13 |
"react-dom": "^19.1.0",
|
| 14 |
"react-markdown": "^10.1.0",
|
| 15 |
"react-scripts": "5.0.1",
|
| 16 |
+
"rehype-katex": "^7.0.1",
|
| 17 |
"remark-gfm": "^4.0.1",
|
| 18 |
+
"remark-math": "^6.0.0",
|
| 19 |
"web-vitals": "^2.1.4"
|
| 20 |
},
|
| 21 |
"scripts": {
|
phd-advisor-frontend/src/App.js
CHANGED
|
@@ -39,7 +39,7 @@ function App() {
|
|
| 39 |
};
|
| 40 |
|
| 41 |
const navigateToCanvas = (canvasView) => {
|
| 42 |
-
if (
|
| 43 |
localStorage.setItem('canvas-view-v2', canvasView);
|
| 44 |
}
|
| 45 |
setCurrentView('canvas');
|
|
|
|
| 39 |
};
|
| 40 |
|
| 41 |
const navigateToCanvas = (canvasView) => {
|
| 42 |
+
if (['insights', 'workspace', 'deliverables'].includes(canvasView)) {
|
| 43 |
localStorage.setItem('canvas-view-v2', canvasView);
|
| 44 |
}
|
| 45 |
setCurrentView('canvas');
|
phd-advisor-frontend/src/components/canvas/CanvasDeliverables.js
CHANGED
|
@@ -1,16 +1,32 @@
|
|
| 1 |
-
// Deliverables view —
|
| 2 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import React, { useState, useMemo, useEffect, useRef } from 'react';
|
| 4 |
import ReactMarkdown from 'react-markdown';
|
| 5 |
import remarkGfm from 'remark-gfm';
|
|
|
|
|
|
|
|
|
|
| 6 |
import Icon from './CanvasIcon';
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
const fireToast = (msg, kind = 'success') =>
|
| 9 |
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg, kind } }));
|
| 10 |
|
| 11 |
-
const STORE_KEY = 'canvas-deliverables-
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
//
|
|
|
|
|
|
|
| 14 |
const TEMPLATES = [
|
| 15 |
{
|
| 16 |
id: 'research-paper',
|
|
@@ -27,10 +43,24 @@ const TEMPLATES = [
|
|
| 27 |
{ id: 'refs', name: 'References', target: 0, hint: 'Bibliography list. Drop @keys here from the Bibliography widget.', checks: [] },
|
| 28 |
],
|
| 29 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
{
|
| 31 |
id: 'nsf-grfp',
|
| 32 |
name: 'NSF GRFP',
|
| 33 |
-
desc: 'Personal Statement (3 pages) + Research Plan (2 pages)',
|
| 34 |
icon: 'award',
|
| 35 |
mode: 'document',
|
| 36 |
sections: [
|
|
@@ -66,22 +96,90 @@ const TEMPLATES = [
|
|
| 66 |
],
|
| 67 |
},
|
| 68 |
{
|
| 69 |
-
id: '
|
| 70 |
-
name: '
|
| 71 |
-
desc: '
|
| 72 |
-
icon: '
|
| 73 |
-
mode: '
|
| 74 |
sections: [
|
| 75 |
-
{ id: '
|
| 76 |
-
{ id: '
|
| 77 |
-
{ id: 'methods', name: 'Methods', target:
|
| 78 |
-
{ id: 'results', name: 'Results', target:
|
| 79 |
-
{ id: 'discussion', name: 'Discussion', target:
|
|
|
|
| 80 |
],
|
| 81 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
];
|
| 83 |
|
| 84 |
-
//
|
|
|
|
|
|
|
| 85 |
const CHECKS = {
|
| 86 |
hasNumber: { test: (s) => /\d/.test(s), label: 'Mentions at least one number' },
|
| 87 |
hasCitation: { test: (s) => /@\w+/.test(s), label: 'Cites at least one source (@key)' },
|
|
@@ -89,40 +187,36 @@ const CHECKS = {
|
|
| 89 |
hasGap: { test: (s) => /\b(gap|lack|missing|unknown|unclear|despite)/i.test(s), label: 'Names a gap' },
|
| 90 |
hasFigure: { test: (s) => /\b(fig(ure)?|table)\.?\s*\d/i.test(s), label: 'References a figure or table' },
|
| 91 |
hasLimit: { test: (s) => /\b(limit|caveat|however|future work|did not|cannot)/i.test(s), label: 'Acknowledges a limit' },
|
| 92 |
-
hasBroaderImpacts: { test: (s) => /\b(broader impact|outreach|community|underrepresented|access)/i.test(s), label: 'Addresses broader impacts' },
|
| 93 |
-
hasHypothesis: { test: (s) => /\b(hypothes|predict)/i.test(s), label: 'States a hypothesis or
|
| 94 |
hasMerit: { test: (s) => /\b(intellectual merit|novel|advances|contribut)/i.test(s), label: 'Frames intellectual merit' },
|
| 95 |
};
|
| 96 |
|
| 97 |
const wordCount = (s) => (s || '').trim().split(/\s+/).filter(Boolean).length;
|
|
|
|
| 98 |
|
| 99 |
-
//
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
const exportLatex = (template, sections) =>
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
...template.sections.map(s => `<section><h2>${s.name}</h2><p>${(sections[s.id] || '').replace(/\n/g, '<br>')}</p></section>`),
|
| 122 |
-
'</body></html>',
|
| 123 |
-
].join('\n');
|
| 124 |
-
};
|
| 125 |
-
|
| 126 |
const downloadFile = (filename, mime, contents) => {
|
| 127 |
const blob = new Blob([contents], { type: mime });
|
| 128 |
const a = document.createElement('a');
|
|
@@ -131,36 +225,119 @@ const downloadFile = (filename, mime, contents) => {
|
|
| 131 |
a.click();
|
| 132 |
};
|
| 133 |
|
| 134 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
const DeliverablesView = ({ allStates }) => {
|
| 136 |
-
const [store, setStore] = useState(
|
| 137 |
-
try { return JSON.parse(localStorage.getItem(STORE_KEY) || '{}'); } catch { return {}; }
|
| 138 |
-
});
|
| 139 |
useEffect(() => { localStorage.setItem(STORE_KEY, JSON.stringify(store)); }, [store]);
|
| 140 |
|
| 141 |
-
const
|
| 142 |
-
const template = TEMPLATES.find(t => t.id ===
|
| 143 |
-
const sections =
|
| 144 |
const [activeSectionId, setActiveSectionId] = useState(template?.sections[0]?.id);
|
| 145 |
const [generatingAi, setGeneratingAi] = useState(false);
|
|
|
|
| 146 |
|
| 147 |
-
//
|
| 148 |
-
// template switch, etc.). Falls back to the first section.
|
| 149 |
useEffect(() => {
|
| 150 |
if (!template) return;
|
| 151 |
const valid = template.sections.some(s => s.id === activeSectionId);
|
| 152 |
if (!valid) setActiveSectionId(template.sections[0].id);
|
| 153 |
-
}, [
|
| 154 |
-
|
| 155 |
-
//
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
const runAiPass = () => {
|
| 165 |
setGeneratingAi(true);
|
| 166 |
setTimeout(() => {
|
|
@@ -171,32 +348,36 @@ const DeliverablesView = ({ allStates }) => {
|
|
| 171 |
if (wc < s.target * 0.3) return { sectionId: s.id, msg: `Thin (${wc} words). Target ${s.target}.` };
|
| 172 |
return { sectionId: s.id, msg: `Looks reasonable for length (${wc} words). LLM-pass would suggest specifics here.` };
|
| 173 |
});
|
| 174 |
-
setStore(
|
| 175 |
-
...prev,
|
| 176 |
-
templates: { ...prev.templates, [activeId]: { ...sections, _aiNotes: notes } },
|
| 177 |
-
}));
|
| 178 |
setGeneratingAi(false);
|
| 179 |
fireToast('AI pass complete (stub)');
|
| 180 |
}, 700);
|
| 181 |
};
|
| 182 |
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
-
const
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
[activeId]: { ...(prev.templates?.[activeId] || {}), [id]: value },
|
| 194 |
-
},
|
| 195 |
-
}));
|
| 196 |
};
|
| 197 |
|
|
|
|
| 198 |
const exportAs = (format) => {
|
| 199 |
-
const filename = `${
|
| 200 |
const mime = format === 'html' ? 'text/html' : 'text/plain';
|
| 201 |
const contents = format === 'markdown' ? exportMarkdown(template, sections)
|
| 202 |
: format === 'latex' ? exportLatex(template, sections)
|
|
@@ -205,58 +386,74 @@ const DeliverablesView = ({ allStates }) => {
|
|
| 205 |
fireToast(`Exported ${filename}`);
|
| 206 |
};
|
| 207 |
|
| 208 |
-
//
|
| 209 |
const totalWords = template ? template.sections.reduce((sum, s) => sum + wordCount(sections[s.id]), 0) : 0;
|
| 210 |
const totalTarget = template ? template.sections.reduce((sum, s) => sum + s.target, 0) : 0;
|
|
|
|
| 211 |
|
| 212 |
-
//
|
| 213 |
-
|
| 214 |
-
const items = [];
|
| 215 |
-
(allStates?.bibliography?.entries || []).forEach(e => items.push({ kind: 'cite', label: `${e.title}`, snippet: ` (${e.authors}, ${e.year}; @${e.key})` }));
|
| 216 |
-
(allStates?.highlights?.items || []).forEach(h => items.push({ kind: 'quote', label: h.text.slice(0, 60), snippet: `"${h.text}"${h.citeKey ? ` (@${h.citeKey})` : ''}` }));
|
| 217 |
-
(allStates?.outline?.items || []).forEach(o => items.push({ kind: 'outline', label: o.text || '(empty)', snippet: '\n' + ' '.repeat(o.depth) + '- ' + (o.text || '') }));
|
| 218 |
-
(allStates?.writing?.chapters || []).forEach(c => items.push({ kind: 'draft', label: c.name, snippet: c.draft || '' }));
|
| 219 |
-
return items;
|
| 220 |
-
}, [allStates]);
|
| 221 |
-
|
| 222 |
-
const insertIntoActive = (snippet) => {
|
| 223 |
-
if (!activeSectionId) return;
|
| 224 |
-
const cur = sections[activeSectionId] || '';
|
| 225 |
-
updateSection(activeSectionId, cur + (cur && !cur.endsWith('\n') ? ' ' : '') + snippet);
|
| 226 |
-
fireToast('Inserted into ' + template.sections.find(s => s.id === activeSectionId)?.name);
|
| 227 |
-
};
|
| 228 |
-
|
| 229 |
-
// Empty state — pick a template
|
| 230 |
-
if (!template) {
|
| 231 |
return (
|
| 232 |
<>
|
| 233 |
<div className="page-header">
|
| 234 |
<div>
|
| 235 |
<h1 className="page-title">Deliverables</h1>
|
| 236 |
-
<div className="page-sub">
|
|
|
|
|
|
|
|
|
|
| 237 |
</div>
|
| 238 |
</div>
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
</div>
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
const onUpload = async (file) => {
|
| 254 |
-
const text = await file.text();
|
| 255 |
-
const res = await fetch(`${API}/api/canvas/outline-from-brief`, { method:'POST', body: text });
|
| 256 |
-
const { template, sections } = await res.json();
|
| 257 |
-
// Inject as a new template into TEMPLATES at runtime, then pickTemplate(template.id);
|
| 258 |
-
};
|
| 259 |
-
*/}
|
| 260 |
<div className="canvas-presets" style={{ marginTop: 18 }}>
|
| 261 |
<div className="canvas-presets-head">
|
| 262 |
<div className="canvas-presets-title">From a project brief</div>
|
|
@@ -270,51 +467,118 @@ const DeliverablesView = ({ allStates }) => {
|
|
| 270 |
);
|
| 271 |
}
|
| 272 |
|
| 273 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
-
// Shared header + insertables panel — used by all editor modes.
|
| 276 |
const Header = (
|
| 277 |
<div className="page-header">
|
| 278 |
<div>
|
| 279 |
-
<button className="btn btn-ghost" style={{ padding: '4px 8px', fontSize: 12, marginBottom: 4, color: 'var(--canvas-text-3)' }} onClick={
|
| 280 |
-
<Icon name="back" size={12}/>
|
| 281 |
</button>
|
| 282 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
<div className="page-sub">
|
| 284 |
-
{totalWords} / {totalTarget} words · {template.sections.length} {template.mode === 'slides' ? 'slides' : 'sections'}
|
| 285 |
</div>
|
| 286 |
</div>
|
| 287 |
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'flex-start' }}>
|
|
|
|
|
|
|
|
|
|
| 288 |
<button className="btn btn-ghost" onClick={runAiPass} disabled={generatingAi} title="AI check (stub)">
|
| 289 |
{generatingAi ? <><div className="spinner"/></> : <Icon name="sparkles" size={13}/>}
|
| 290 |
AI check
|
| 291 |
</button>
|
| 292 |
-
<
|
| 293 |
-
<
|
| 294 |
-
|
| 295 |
-
<
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
|
|
|
| 303 |
</div>
|
| 304 |
</div>
|
| 305 |
);
|
| 306 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
const InsertPanel = (
|
| 308 |
<div className="deliverable-insertables">
|
| 309 |
-
<
|
| 310 |
-
|
|
|
|
| 311 |
</div>
|
| 312 |
-
{
|
| 313 |
<div style={{ padding: 12, fontSize: 11.5, color: 'var(--canvas-text-3)', background: 'var(--canvas-surface)', border: '1px dashed var(--canvas-border-2)', borderRadius: 7 }}>
|
| 314 |
-
Add a Bibliography, Highlights, Outline, or Writing widget to your canvas
|
| 315 |
</div>
|
| 316 |
)}
|
| 317 |
-
{
|
| 318 |
<button key={i} onClick={() => insertIntoActive(it.snippet)} className="canvas-insert-row">
|
| 319 |
<span className="tag-pill">{it.kind}</span>
|
| 320 |
<span style={{ flex: 1, fontSize: 11.5, color: 'var(--canvas-text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{it.label}</span>
|
|
@@ -324,20 +588,47 @@ const DeliverablesView = ({ allStates }) => {
|
|
| 324 |
</div>
|
| 325 |
);
|
| 326 |
|
| 327 |
-
// ----------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
if (template.mode === 'slides') {
|
| 329 |
const activeIdx = template.sections.findIndex(s => s.id === activeSectionId);
|
| 330 |
const active = template.sections[activeIdx] || template.sections[0];
|
| 331 |
const text = sections[active.id] || '';
|
| 332 |
const aiForSlide = aiNotes && aiNotes.find(n => n.sectionId === active.id);
|
| 333 |
-
// Render body as bullet points if it contains line breaks; otherwise as a paragraph.
|
| 334 |
const lines = text.split(/\n+/).map(l => l.trim()).filter(Boolean);
|
| 335 |
|
| 336 |
return (
|
| 337 |
<>
|
|
|
|
| 338 |
{Header}
|
|
|
|
| 339 |
<div className="deliverable-slides-grid">
|
| 340 |
-
{/* Slide thumbnails */}
|
| 341 |
<div className="slide-thumbs">
|
| 342 |
<div style={{ fontSize: 10, color: 'var(--canvas-text-4)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600, padding: '0 4px 6px' }}>
|
| 343 |
{template.sections.length} slides
|
|
@@ -361,8 +652,6 @@ const DeliverablesView = ({ allStates }) => {
|
|
| 361 |
);
|
| 362 |
})}
|
| 363 |
</div>
|
| 364 |
-
|
| 365 |
-
{/* Big slide canvas */}
|
| 366 |
<div>
|
| 367 |
<div className="slide-canvas-wrap">
|
| 368 |
<div className="slide-canvas">
|
|
@@ -379,8 +668,6 @@ const DeliverablesView = ({ allStates }) => {
|
|
| 379 |
<div className="slide-canvas-footer">{activeIdx + 1} / {template.sections.length}</div>
|
| 380 |
</div>
|
| 381 |
</div>
|
| 382 |
-
|
| 383 |
-
{/* Edit pane below the canvas (so the slide stays the focus) */}
|
| 384 |
<div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
| 385 |
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
| 386 |
<span style={{ fontSize: 11, color: 'var(--canvas-text-4)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600 }}>
|
|
@@ -392,12 +679,11 @@ const DeliverablesView = ({ allStates }) => {
|
|
| 392 |
{wordCount(text)}{active.target ? `/${active.target}` : ''} words
|
| 393 |
</span>
|
| 394 |
</div>
|
| 395 |
-
<
|
| 396 |
-
className="textarea"
|
| 397 |
value={text}
|
| 398 |
-
onChange={
|
| 399 |
placeholder={active.hint}
|
| 400 |
-
|
| 401 |
/>
|
| 402 |
{(active.checks || []).length > 0 && (
|
| 403 |
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
|
@@ -415,27 +701,30 @@ const DeliverablesView = ({ allStates }) => {
|
|
| 415 |
</div>
|
| 416 |
)}
|
| 417 |
{aiForSlide && (
|
| 418 |
-
<div className="
|
| 419 |
-
<
|
| 420 |
-
|
|
|
|
|
|
|
|
|
|
| 421 |
</div>
|
| 422 |
)}
|
| 423 |
</div>
|
| 424 |
</div>
|
| 425 |
-
|
| 426 |
{InsertPanel}
|
| 427 |
</div>
|
| 428 |
</>
|
| 429 |
);
|
| 430 |
}
|
| 431 |
|
| 432 |
-
// ---------- PAPER / DOCUMENT MODE — Notion
|
| 433 |
const paperLike = template.mode === 'paper';
|
| 434 |
return (
|
| 435 |
<>
|
|
|
|
| 436 |
{Header}
|
|
|
|
| 437 |
<div className="notion-deliverable-grid">
|
| 438 |
-
{/* TOC sidebar — subtle, no boxes */}
|
| 439 |
<div className="notion-toc">
|
| 440 |
<div className="notion-toc-label">On this page</div>
|
| 441 |
{template.sections.map(s => {
|
|
@@ -455,25 +744,23 @@ const DeliverablesView = ({ allStates }) => {
|
|
| 455 |
})}
|
| 456 |
</div>
|
| 457 |
|
| 458 |
-
{/* The page itself — what you see is what you edit */}
|
| 459 |
<div className={`notion-page-wrap ${paperLike ? 'paper' : ''}`}>
|
| 460 |
<div className={`notion-page ${paperLike ? 'serif' : ''}`}>
|
| 461 |
-
<h1 className="notion-page-title">{
|
| 462 |
<div className="notion-page-meta">
|
| 463 |
-
{totalWords} words · {template.sections.length} sections{paperLike ? ' · academic paper' : ''}
|
| 464 |
</div>
|
| 465 |
{template.sections.map(s => {
|
| 466 |
const text = sections[s.id] || '';
|
| 467 |
const aiForSection = aiNotes ? aiNotes.find(n => n.sectionId === s.id) : null;
|
| 468 |
-
const failed = (s.checks || []).filter(c => CHECKS[c] && !CHECKS[c].test(text));
|
| 469 |
return (
|
| 470 |
<div key={s.id} id={`notion-section-${s.id}`} className="notion-block">
|
| 471 |
<h2 className={`notion-h2 ${paperLike ? 'serif' : ''}`}>{s.name}</h2>
|
| 472 |
-
|
| 473 |
-
<NotionTextarea
|
| 474 |
value={text}
|
| 475 |
onChange={(v) => updateSection(s.id, v)}
|
| 476 |
placeholder={`Start writing ${s.name.toLowerCase()}…`}
|
|
|
|
| 477 |
serif={paperLike}
|
| 478 |
/>
|
| 479 |
{(s.checks || []).length > 0 && (
|
|
@@ -517,24 +804,356 @@ const DeliverablesView = ({ allStates }) => {
|
|
| 517 |
);
|
| 518 |
};
|
| 519 |
|
| 520 |
-
//
|
| 521 |
-
//
|
| 522 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
const ref = useRef(null);
|
|
|
|
|
|
|
| 524 |
useEffect(() => {
|
| 525 |
-
if (!ref.current) return;
|
| 526 |
ref.current.style.height = 'auto';
|
| 527 |
ref.current.style.height = ref.current.scrollHeight + 'px';
|
| 528 |
-
}, [value]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
return (
|
| 530 |
-
<
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
);
|
| 539 |
}
|
| 540 |
|
|
|
|
| 1 |
+
// Deliverables view — multi-project PhD deliverable center.
|
| 2 |
+
// Each "project" is a draft of a template (paper, slides, poster, CV, etc.).
|
| 3 |
+
// You can keep many projects in flight, switch between them, version-rollback,
|
| 4 |
+
// drag in citations from your Bibliography or arXiv, embed images, render math,
|
| 5 |
+
// and export to Markdown / LaTeX / HTML / Print.
|
| 6 |
+
// AI passes are stubbed (need LLM endpoint) but every static signal works today.
|
| 7 |
import React, { useState, useMemo, useEffect, useRef } from 'react';
|
| 8 |
import ReactMarkdown from 'react-markdown';
|
| 9 |
import remarkGfm from 'remark-gfm';
|
| 10 |
+
import remarkMath from 'remark-math';
|
| 11 |
+
import rehypeKatex from 'rehype-katex';
|
| 12 |
+
import 'katex/dist/katex.min.css';
|
| 13 |
import Icon from './CanvasIcon';
|
| 14 |
|
| 15 |
+
// Markdown plugins shared across all rendered blocks. remark-math + rehype-katex
|
| 16 |
+
// give us real LaTeX math (`$...$` inline, `$$...$$` block) inside any preview.
|
| 17 |
+
const REMARK_PLUGINS = [remarkGfm, remarkMath];
|
| 18 |
+
const REHYPE_PLUGINS = [rehypeKatex];
|
| 19 |
+
|
| 20 |
const fireToast = (msg, kind = 'success') =>
|
| 21 |
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg, kind } }));
|
| 22 |
|
| 23 |
+
const STORE_KEY = 'canvas-deliverables-v2';
|
| 24 |
+
const MAX_VERSIONS = 10;
|
| 25 |
+
const newId = (p) => p + Math.random().toString(36).slice(2, 8);
|
| 26 |
|
| 27 |
+
// ============================================================================
|
| 28 |
+
// Templates
|
| 29 |
+
// ============================================================================
|
| 30 |
const TEMPLATES = [
|
| 31 |
{
|
| 32 |
id: 'research-paper',
|
|
|
|
| 43 |
{ id: 'refs', name: 'References', target: 0, hint: 'Bibliography list. Drop @keys here from the Bibliography widget.', checks: [] },
|
| 44 |
],
|
| 45 |
},
|
| 46 |
+
{
|
| 47 |
+
id: 'thesis-chapter',
|
| 48 |
+
name: 'Thesis Chapter',
|
| 49 |
+
desc: 'Standard chapter scaffolding for a dissertation.',
|
| 50 |
+
icon: 'book',
|
| 51 |
+
mode: 'paper',
|
| 52 |
+
sections: [
|
| 53 |
+
{ id: 'overview', name: 'Overview', target: 200, hint: 'What this chapter does and why it\'s here.', checks: [] },
|
| 54 |
+
{ id: 'background', name: 'Background', target: 1500, hint: 'Lit review focused on this chapter\'s question.', checks: ['hasCitation'] },
|
| 55 |
+
{ id: 'methods', name: 'Methods', target: 1500, hint: 'Reproducibility-first.', checks: ['hasCitation', 'hasNumber'] },
|
| 56 |
+
{ id: 'results', name: 'Results', target: 2000, hint: 'Findings + figures.', checks: ['hasFigure', 'hasNumber'] },
|
| 57 |
+
{ id: 'discussion', name: 'Discussion', target: 1500, hint: 'How it fits the larger thesis.', checks: ['hasLimit'] },
|
| 58 |
+
],
|
| 59 |
+
},
|
| 60 |
{
|
| 61 |
id: 'nsf-grfp',
|
| 62 |
name: 'NSF GRFP',
|
| 63 |
+
desc: 'Personal Statement (3 pages) + Research Plan (2 pages).',
|
| 64 |
icon: 'award',
|
| 65 |
mode: 'document',
|
| 66 |
sections: [
|
|
|
|
| 96 |
],
|
| 97 |
},
|
| 98 |
{
|
| 99 |
+
id: 'poster',
|
| 100 |
+
name: 'Conference Poster',
|
| 101 |
+
desc: '4-quadrant scientific poster: Intro · Methods · Results · Discussion.',
|
| 102 |
+
icon: 'layout',
|
| 103 |
+
mode: 'poster',
|
| 104 |
sections: [
|
| 105 |
+
{ id: 'title', name: 'Title & Authors', target: 30, hint: 'Title, your name, advisor, affiliation.', checks: [] },
|
| 106 |
+
{ id: 'intro', name: 'Introduction', target: 200, hint: 'Question, gap, why-care.', checks: ['hasGap'] },
|
| 107 |
+
{ id: 'methods', name: 'Methods', target: 200, hint: 'High-level: subjects, design, analysis.', checks: ['hasNumber'] },
|
| 108 |
+
{ id: 'results', name: 'Results', target: 250, hint: 'Headline finding + 1–2 figures.', checks: ['hasFigure', 'hasNumber'] },
|
| 109 |
+
{ id: 'discussion', name: 'Discussion', target: 200, hint: 'What it means + next steps.', checks: ['hasLimit'] },
|
| 110 |
+
{ id: 'refs', name: 'References / Acks', target: 80, hint: '5–10 citations + funding + contact.', checks: ['hasCitation'] },
|
| 111 |
],
|
| 112 |
},
|
| 113 |
+
{
|
| 114 |
+
id: 'cv',
|
| 115 |
+
name: 'Academic CV',
|
| 116 |
+
desc: 'Standard sections: Education · Pubs · Talks · Awards · Service · Skills.',
|
| 117 |
+
icon: 'user',
|
| 118 |
+
mode: 'document',
|
| 119 |
+
sections: [
|
| 120 |
+
{ id: 'header', name: 'Header', target: 40, hint: 'Name, position, affiliation, contact.', checks: [] },
|
| 121 |
+
{ id: 'education', name: 'Education', target: 100, hint: 'Most recent first. Degree · Year · Institution.', checks: [] },
|
| 122 |
+
{ id: 'publications', name: 'Publications', target: 300, hint: 'Drop @keys from Bibliography. Group by type if needed.', checks: ['hasCitation'] },
|
| 123 |
+
{ id: 'talks', name: 'Invited talks & posters', target: 150, hint: 'Title · Venue · Year.', checks: [] },
|
| 124 |
+
{ id: 'awards', name: 'Awards & funding', target: 100, hint: 'Most recent first. Amount + year if applicable.', checks: [] },
|
| 125 |
+
{ id: 'service', name: 'Service & teaching', target: 100, hint: 'Reviewing, mentorship, TA roles.', checks: [] },
|
| 126 |
+
{ id: 'skills', name: 'Skills', target: 60, hint: 'Methods, languages, software.', checks: [] },
|
| 127 |
+
],
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
id: 'cover-letter',
|
| 131 |
+
name: 'Cover Letter',
|
| 132 |
+
desc: 'For job applications, journal submissions, or postdoc inquiries.',
|
| 133 |
+
icon: 'send',
|
| 134 |
+
mode: 'document',
|
| 135 |
+
sections: [
|
| 136 |
+
{ id: 'header', name: 'Header', target: 40, hint: 'Date, recipient, salutation.', checks: [] },
|
| 137 |
+
{ id: 'opener', name: 'Opening paragraph', target: 100, hint: 'Why you\'re writing + the position/journal.', checks: [] },
|
| 138 |
+
{ id: 'body', name: 'Why me', target: 250, hint: 'Specific achievements that match the call. Numbers > adjectives.', checks: ['hasNumber'] },
|
| 139 |
+
{ id: 'fit', name: 'Why this place', target: 150, hint: 'What about this group / journal makes it the right fit.', checks: [] },
|
| 140 |
+
{ id: 'close', name: 'Close', target: 60, hint: 'Thanks + next step + signature.', checks: [] },
|
| 141 |
+
],
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
id: 'research-statement',
|
| 145 |
+
name: 'Research Statement',
|
| 146 |
+
desc: 'For faculty applications: past work, current direction, future arc.',
|
| 147 |
+
icon: 'sparkles',
|
| 148 |
+
mode: 'document',
|
| 149 |
+
sections: [
|
| 150 |
+
{ id: 'overview', name: 'Overview', target: 200, hint: 'One paragraph: your research identity in 2–3 sentences.', checks: [] },
|
| 151 |
+
{ id: 'past', name: 'Past work', target: 600, hint: 'What you\'ve done. Lead with results, cite your own papers.', checks: ['hasCitation', 'hasFinding'] },
|
| 152 |
+
{ id: 'current', name: 'Current direction', target: 400, hint: 'What you\'re working on now and why it matters.', checks: ['hasGap'] },
|
| 153 |
+
{ id: 'future', name: 'Future research', target: 600, hint: '3–5 year arc. 1–2 funding-ready specific aims.', checks: ['hasHypothesis'] },
|
| 154 |
+
{ id: 'broader', name: 'Broader impacts', target: 200, hint: 'Outreach, mentorship, what your group will look like.', checks: ['hasBroaderImpacts'] },
|
| 155 |
+
],
|
| 156 |
+
},
|
| 157 |
+
];
|
| 158 |
+
|
| 159 |
+
// ============================================================================
|
| 160 |
+
// Slash command catalog — typed at the start of a line in any section editor.
|
| 161 |
+
// ============================================================================
|
| 162 |
+
const SLASH_COMMANDS = [
|
| 163 |
+
{ id: 'h2', label: 'Heading', kind: 'block', icon: 'list', insert: () => '## Heading\n' },
|
| 164 |
+
{ id: 'h3', label: 'Subheading', kind: 'block', icon: 'list', insert: () => '### Subheading\n' },
|
| 165 |
+
{ id: 'list', label: 'Bullet list', kind: 'block', icon: 'list', insert: () => '- ' },
|
| 166 |
+
{ id: 'todo', label: 'To-do', kind: 'block', icon: 'task', insert: () => '- [ ] ' },
|
| 167 |
+
{ id: 'numbered', label: 'Numbered list', kind: 'block', icon: 'list', insert: () => '1. ' },
|
| 168 |
+
{ id: 'quote', label: 'Quote', kind: 'block', icon: 'cite', insert: () => '> ' },
|
| 169 |
+
{ id: 'callout', label: 'Callout', kind: 'block', icon: 'sparkles', insert: () => '> [!note]\n> ' },
|
| 170 |
+
{ id: 'divider', label: 'Divider', kind: 'block', icon: 'list', insert: () => '\n---\n\n' },
|
| 171 |
+
{ id: 'code', label: 'Code block', kind: 'block', icon: 'flask', insert: () => '```\n\n```\n' },
|
| 172 |
+
{ id: 'math', label: 'Equation', kind: 'block', icon: 'flask', insert: () => '$$\nE = mc^2\n$$\n' },
|
| 173 |
+
{ id: 'inline-math', label: 'Inline equation', kind: 'inline', icon: 'flask', insert: () => '$x^2$' },
|
| 174 |
+
{ id: 'image', label: 'Image (paste URL)', kind: 'block', icon: 'download', insert: () => '' },
|
| 175 |
+
{ id: 'cite', label: 'Citation @key', kind: 'inline', icon: 'book', insert: () => '(@key)' },
|
| 176 |
+
{ id: 'bold', label: 'Bold', kind: 'inline', icon: 'pencil', insert: () => '**bold**' },
|
| 177 |
+
{ id: 'italic', label: 'Italic', kind: 'inline', icon: 'pencil', insert: () => '*italic*' },
|
| 178 |
];
|
| 179 |
|
| 180 |
+
// ============================================================================
|
| 181 |
+
// Static "missing" checks
|
| 182 |
+
// ============================================================================
|
| 183 |
const CHECKS = {
|
| 184 |
hasNumber: { test: (s) => /\d/.test(s), label: 'Mentions at least one number' },
|
| 185 |
hasCitation: { test: (s) => /@\w+/.test(s), label: 'Cites at least one source (@key)' },
|
|
|
|
| 187 |
hasGap: { test: (s) => /\b(gap|lack|missing|unknown|unclear|despite)/i.test(s), label: 'Names a gap' },
|
| 188 |
hasFigure: { test: (s) => /\b(fig(ure)?|table)\.?\s*\d/i.test(s), label: 'References a figure or table' },
|
| 189 |
hasLimit: { test: (s) => /\b(limit|caveat|however|future work|did not|cannot)/i.test(s), label: 'Acknowledges a limit' },
|
| 190 |
+
hasBroaderImpacts: { test: (s) => /\b(broader impact|outreach|community|underrepresented|access|teaching)/i.test(s), label: 'Addresses broader impacts' },
|
| 191 |
+
hasHypothesis: { test: (s) => /\b(hypothes|predict|aim ?\d)/i.test(s), label: 'States a hypothesis or aim' },
|
| 192 |
hasMerit: { test: (s) => /\b(intellectual merit|novel|advances|contribut)/i.test(s), label: 'Frames intellectual merit' },
|
| 193 |
};
|
| 194 |
|
| 195 |
const wordCount = (s) => (s || '').trim().split(/\s+/).filter(Boolean).length;
|
| 196 |
+
const readingMinutes = (n) => Math.max(1, Math.round(n / 220));
|
| 197 |
|
| 198 |
+
// ============================================================================
|
| 199 |
+
// Exporters
|
| 200 |
+
// ============================================================================
|
| 201 |
+
const exportMarkdown = (template, sections) => [
|
| 202 |
+
`# ${template.name}\n`,
|
| 203 |
+
...template.sections.map(s => `## ${s.name}\n\n${sections[s.id] || ''}\n`),
|
| 204 |
+
].join('\n');
|
| 205 |
+
const exportLatex = (template, sections) => [
|
| 206 |
+
'\\documentclass{article}',
|
| 207 |
+
`\\title{${template.name}}`,
|
| 208 |
+
'\\begin{document}',
|
| 209 |
+
'\\maketitle',
|
| 210 |
+
...template.sections.map(s => `\n\\section{${s.name}}\n${sections[s.id] || ''}\n`),
|
| 211 |
+
'\\end{document}',
|
| 212 |
+
].join('\n');
|
| 213 |
+
const exportHtml = (template, sections) => [
|
| 214 |
+
'<!doctype html>',
|
| 215 |
+
`<html><head><title>${template.name}</title></head><body>`,
|
| 216 |
+
`<h1>${template.name}</h1>`,
|
| 217 |
+
...template.sections.map(s => `<section><h2>${s.name}</h2><p>${(sections[s.id] || '').replace(/\n/g, '<br>')}</p></section>`),
|
| 218 |
+
'</body></html>',
|
| 219 |
+
].join('\n');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
const downloadFile = (filename, mime, contents) => {
|
| 221 |
const blob = new Blob([contents], { type: mime });
|
| 222 |
const a = document.createElement('a');
|
|
|
|
| 225 |
a.click();
|
| 226 |
};
|
| 227 |
|
| 228 |
+
// ============================================================================
|
| 229 |
+
// Project store — multi-project (was: single-template). Migrates v1 if found.
|
| 230 |
+
// ============================================================================
|
| 231 |
+
const loadStore = () => {
|
| 232 |
+
try {
|
| 233 |
+
const v2 = JSON.parse(localStorage.getItem(STORE_KEY) || 'null');
|
| 234 |
+
if (v2) return v2;
|
| 235 |
+
// Migrate v1: turn each templateId entry into a project.
|
| 236 |
+
const v1 = JSON.parse(localStorage.getItem('canvas-deliverables-v1') || '{}');
|
| 237 |
+
if (v1 && v1.templates) {
|
| 238 |
+
const projects = {};
|
| 239 |
+
let activeId;
|
| 240 |
+
Object.entries(v1.templates).forEach(([tid, sec]) => {
|
| 241 |
+
const id = newId('p-');
|
| 242 |
+
const { _aiNotes, ...rest } = sec;
|
| 243 |
+
projects[id] = {
|
| 244 |
+
id,
|
| 245 |
+
name: TEMPLATES.find(t => t.id === tid)?.name || tid,
|
| 246 |
+
templateId: tid,
|
| 247 |
+
sections: rest,
|
| 248 |
+
versions: [],
|
| 249 |
+
aiNotes: _aiNotes || null,
|
| 250 |
+
createdAt: Date.now(),
|
| 251 |
+
};
|
| 252 |
+
if (tid === v1.activeTemplateId) activeId = id;
|
| 253 |
+
});
|
| 254 |
+
return { activeProjectId: activeId, projects };
|
| 255 |
+
}
|
| 256 |
+
} catch { /* fallthrough */ }
|
| 257 |
+
return { activeProjectId: null, projects: {} };
|
| 258 |
+
};
|
| 259 |
+
|
| 260 |
+
// ============================================================================
|
| 261 |
+
// Main view
|
| 262 |
+
// ============================================================================
|
| 263 |
const DeliverablesView = ({ allStates }) => {
|
| 264 |
+
const [store, setStore] = useState(loadStore);
|
|
|
|
|
|
|
| 265 |
useEffect(() => { localStorage.setItem(STORE_KEY, JSON.stringify(store)); }, [store]);
|
| 266 |
|
| 267 |
+
const project = store.projects[store.activeProjectId] || null;
|
| 268 |
+
const template = project ? TEMPLATES.find(t => t.id === project.templateId) : null;
|
| 269 |
+
const sections = project?.sections || {};
|
| 270 |
const [activeSectionId, setActiveSectionId] = useState(template?.sections[0]?.id);
|
| 271 |
const [generatingAi, setGeneratingAi] = useState(false);
|
| 272 |
+
const [historyOpen, setHistoryOpen] = useState(false);
|
| 273 |
|
| 274 |
+
// Normalize active section when project/template changes
|
|
|
|
| 275 |
useEffect(() => {
|
| 276 |
if (!template) return;
|
| 277 |
const valid = template.sections.some(s => s.id === activeSectionId);
|
| 278 |
if (!valid) setActiveSectionId(template.sections[0].id);
|
| 279 |
+
}, [project?.id, template, activeSectionId]);
|
| 280 |
+
|
| 281 |
+
// ---------- Project ops ----------
|
| 282 |
+
const createProject = (templateId, name) => {
|
| 283 |
+
const id = newId('p-');
|
| 284 |
+
const t = TEMPLATES.find(x => x.id === templateId);
|
| 285 |
+
setStore(s => ({
|
| 286 |
+
activeProjectId: id,
|
| 287 |
+
projects: {
|
| 288 |
+
...s.projects,
|
| 289 |
+
[id]: { id, name: name || `${t.name} draft`, templateId, sections: {}, versions: [], aiNotes: null, createdAt: Date.now() },
|
| 290 |
+
},
|
| 291 |
+
}));
|
| 292 |
+
fireToast(`New ${t.name} draft created`);
|
| 293 |
+
};
|
| 294 |
+
const switchProject = (id) => setStore(s => ({ ...s, activeProjectId: id }));
|
| 295 |
+
const renameProject = (name) => {
|
| 296 |
+
if (!project) return;
|
| 297 |
+
setStore(s => ({ ...s, projects: { ...s.projects, [project.id]: { ...s.projects[project.id], name } } }));
|
| 298 |
+
};
|
| 299 |
+
const deleteProject = () => {
|
| 300 |
+
if (!project) return;
|
| 301 |
+
if (!window.confirm(`Delete "${project.name}"? This can't be undone.`)) return;
|
| 302 |
+
setStore(s => {
|
| 303 |
+
const { [project.id]: _, ...rest } = s.projects;
|
| 304 |
+
const nextActive = Object.keys(rest)[0] || null;
|
| 305 |
+
return { activeProjectId: nextActive, projects: rest };
|
| 306 |
+
});
|
| 307 |
+
};
|
| 308 |
+
const closeProject = () => setStore(s => ({ ...s, activeProjectId: null }));
|
| 309 |
+
|
| 310 |
+
// ---------- Section ops + auto-versioning ----------
|
| 311 |
+
const updateSection = (id, value) => {
|
| 312 |
+
setStore(s => {
|
| 313 |
+
const proj = s.projects[project.id];
|
| 314 |
+
const oldText = proj.sections[id] || '';
|
| 315 |
+
// Snapshot a version every time a section gains/loses ≥80 chars (rough save cadence)
|
| 316 |
+
const shouldSnap = Math.abs(value.length - oldText.length) >= 80;
|
| 317 |
+
const newVersions = shouldSnap
|
| 318 |
+
? [{ at: Date.now(), sectionId: id, snapshot: { ...proj.sections } }, ...proj.versions].slice(0, MAX_VERSIONS)
|
| 319 |
+
: proj.versions;
|
| 320 |
+
return {
|
| 321 |
+
...s,
|
| 322 |
+
projects: {
|
| 323 |
+
...s.projects,
|
| 324 |
+
[project.id]: { ...proj, sections: { ...proj.sections, [id]: value }, versions: newVersions },
|
| 325 |
+
},
|
| 326 |
+
};
|
| 327 |
+
});
|
| 328 |
+
};
|
| 329 |
+
|
| 330 |
+
const restoreVersion = (v) => {
|
| 331 |
+
if (!window.confirm('Restore this version? Current text will be replaced.')) return;
|
| 332 |
+
setStore(s => ({
|
| 333 |
+
...s,
|
| 334 |
+
projects: { ...s.projects, [project.id]: { ...s.projects[project.id], sections: v.snapshot } },
|
| 335 |
+
}));
|
| 336 |
+
fireToast('Restored');
|
| 337 |
+
};
|
| 338 |
+
|
| 339 |
+
// ---------- AI pass (stub) ----------
|
| 340 |
+
// TODO(LLM): POST {project, sections, canvas} → {notes:[{sectionId,msg}]}
|
| 341 |
const runAiPass = () => {
|
| 342 |
setGeneratingAi(true);
|
| 343 |
setTimeout(() => {
|
|
|
|
| 348 |
if (wc < s.target * 0.3) return { sectionId: s.id, msg: `Thin (${wc} words). Target ${s.target}.` };
|
| 349 |
return { sectionId: s.id, msg: `Looks reasonable for length (${wc} words). LLM-pass would suggest specifics here.` };
|
| 350 |
});
|
| 351 |
+
setStore(s => ({ ...s, projects: { ...s.projects, [project.id]: { ...s.projects[project.id], aiNotes: notes } } }));
|
|
|
|
|
|
|
|
|
|
| 352 |
setGeneratingAi(false);
|
| 353 |
fireToast('AI pass complete (stub)');
|
| 354 |
}, 700);
|
| 355 |
};
|
| 356 |
|
| 357 |
+
// ---------- Insertables: Bibliography + Highlights + Outline + Drafts + arXiv search ----------
|
| 358 |
+
const localInsertables = useMemo(() => {
|
| 359 |
+
const items = [];
|
| 360 |
+
(allStates?.bibliography?.entries || []).forEach(e =>
|
| 361 |
+
items.push({ kind: 'cite', label: e.title, snippet: ` (${e.authors}, ${e.year}; @${e.key})` }));
|
| 362 |
+
(allStates?.highlights?.items || []).forEach(h =>
|
| 363 |
+
items.push({ kind: 'quote', label: h.text.slice(0, 60), snippet: `"${h.text}"${h.citeKey ? ` (@${h.citeKey})` : ''}` }));
|
| 364 |
+
(allStates?.outline?.items || []).forEach(o =>
|
| 365 |
+
items.push({ kind: 'outline', label: o.text || '(empty)', snippet: '\n' + ' '.repeat(o.depth) + '- ' + (o.text || '') }));
|
| 366 |
+
(allStates?.writing?.chapters || []).forEach(c =>
|
| 367 |
+
items.push({ kind: 'draft', label: c.name, snippet: c.draft || '' }));
|
| 368 |
+
return items;
|
| 369 |
+
}, [allStates]);
|
| 370 |
|
| 371 |
+
const insertIntoActive = (snippet) => {
|
| 372 |
+
if (!activeSectionId) return;
|
| 373 |
+
const cur = sections[activeSectionId] || '';
|
| 374 |
+
updateSection(activeSectionId, cur + (cur && !cur.endsWith('\n') ? ' ' : '') + snippet);
|
| 375 |
+
fireToast('Inserted into ' + template.sections.find(s => s.id === activeSectionId)?.name);
|
|
|
|
|
|
|
|
|
|
| 376 |
};
|
| 377 |
|
| 378 |
+
// ---------- Export ----------
|
| 379 |
const exportAs = (format) => {
|
| 380 |
+
const filename = `${project.name.replace(/\s+/g, '_')}.${format === 'latex' ? 'tex' : format === 'markdown' ? 'md' : 'html'}`;
|
| 381 |
const mime = format === 'html' ? 'text/html' : 'text/plain';
|
| 382 |
const contents = format === 'markdown' ? exportMarkdown(template, sections)
|
| 383 |
: format === 'latex' ? exportLatex(template, sections)
|
|
|
|
| 386 |
fireToast(`Exported ${filename}`);
|
| 387 |
};
|
| 388 |
|
| 389 |
+
// ---------- Aggregates ----------
|
| 390 |
const totalWords = template ? template.sections.reduce((sum, s) => sum + wordCount(sections[s.id]), 0) : 0;
|
| 391 |
const totalTarget = template ? template.sections.reduce((sum, s) => sum + s.target, 0) : 0;
|
| 392 |
+
const projectList = Object.values(store.projects).sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
| 393 |
|
| 394 |
+
// ---------- Empty state: project picker / template picker ----------
|
| 395 |
+
if (!project) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
return (
|
| 397 |
<>
|
| 398 |
<div className="page-header">
|
| 399 |
<div>
|
| 400 |
<h1 className="page-title">Deliverables</h1>
|
| 401 |
+
<div className="page-sub">
|
| 402 |
+
Your one-stop deliverable center. Drafts auto-save. Versions kept for rollback.
|
| 403 |
+
{projectList.length > 0 && ` · ${projectList.length} draft${projectList.length === 1 ? '' : 's'} in flight.`}
|
| 404 |
+
</div>
|
| 405 |
</div>
|
| 406 |
</div>
|
| 407 |
+
|
| 408 |
+
{/* Existing projects, if any */}
|
| 409 |
+
{projectList.length > 0 && (
|
| 410 |
+
<div className="canvas-presets" style={{ marginBottom: 24 }}>
|
| 411 |
+
<div className="canvas-presets-head">
|
| 412 |
+
<div className="canvas-presets-title">Continue working</div>
|
| 413 |
+
<div className="canvas-presets-sub">Drafts you've started.</div>
|
| 414 |
+
</div>
|
| 415 |
+
<div className="canvas-presets-grid">
|
| 416 |
+
{projectList.map(p => {
|
| 417 |
+
const t = TEMPLATES.find(t => t.id === p.templateId);
|
| 418 |
+
if (!t) return null;
|
| 419 |
+
const wc = t.sections.reduce((sum, s) => sum + wordCount(p.sections[s.id]), 0);
|
| 420 |
+
const target = t.sections.reduce((sum, s) => sum + s.target, 0);
|
| 421 |
+
return (
|
| 422 |
+
<button key={p.id} className="canvas-preset-card" onClick={() => switchProject(p.id)}>
|
| 423 |
+
<div className="canvas-preset-icon"><Icon name={t.icon} size={18}/></div>
|
| 424 |
+
<div className="canvas-preset-content">
|
| 425 |
+
<div className="canvas-preset-name">{p.name}</div>
|
| 426 |
+
<div className="canvas-preset-desc">{t.name} · {wc}/{target} words</div>
|
| 427 |
+
<div className="canvas-preset-meta">opened {new Date(p.createdAt).toLocaleDateString()}</div>
|
| 428 |
+
</div>
|
| 429 |
+
</button>
|
| 430 |
+
);
|
| 431 |
+
})}
|
| 432 |
+
</div>
|
| 433 |
+
</div>
|
| 434 |
+
)}
|
| 435 |
+
|
| 436 |
+
{/* Templates */}
|
| 437 |
+
<div className="canvas-presets">
|
| 438 |
+
<div className="canvas-presets-head">
|
| 439 |
+
<div className="canvas-presets-title">{projectList.length > 0 ? 'Or start a new draft' : 'Pick a template'}</div>
|
| 440 |
+
<div className="canvas-presets-sub">9 templates · paper, slides, poster, CV, cover letter, and more.</div>
|
| 441 |
+
</div>
|
| 442 |
+
<div className="canvas-presets-grid">
|
| 443 |
+
{TEMPLATES.map(t => (
|
| 444 |
+
<button key={t.id} className="canvas-preset-card" onClick={() => createProject(t.id)}>
|
| 445 |
+
<div className="canvas-preset-icon"><Icon name={t.icon} size={18}/></div>
|
| 446 |
+
<div className="canvas-preset-content">
|
| 447 |
+
<div className="canvas-preset-name">{t.name}</div>
|
| 448 |
+
<div className="canvas-preset-desc">{t.desc}</div>
|
| 449 |
+
<div className="canvas-preset-meta">{t.sections.length} sections · {t.mode}</div>
|
| 450 |
+
</div>
|
| 451 |
+
</button>
|
| 452 |
+
))}
|
| 453 |
+
</div>
|
| 454 |
</div>
|
| 455 |
+
|
| 456 |
+
{/* TODO(LLM): "Upload project brief → AI generates a custom outline" */}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
<div className="canvas-presets" style={{ marginTop: 18 }}>
|
| 458 |
<div className="canvas-presets-head">
|
| 459 |
<div className="canvas-presets-title">From a project brief</div>
|
|
|
|
| 467 |
);
|
| 468 |
}
|
| 469 |
|
| 470 |
+
// ============================================================================
|
| 471 |
+
// Editor: project tabs + header + per-mode editor
|
| 472 |
+
// ============================================================================
|
| 473 |
+
const aiNotes = project.aiNotes;
|
| 474 |
+
|
| 475 |
+
const ProjectTabs = (
|
| 476 |
+
<div className="deliverable-projects">
|
| 477 |
+
{projectList.map(p => {
|
| 478 |
+
const t = TEMPLATES.find(x => x.id === p.templateId);
|
| 479 |
+
return (
|
| 480 |
+
<button
|
| 481 |
+
key={p.id}
|
| 482 |
+
className={`deliverable-project-tab ${p.id === project.id ? 'active' : ''}`}
|
| 483 |
+
onClick={() => switchProject(p.id)}
|
| 484 |
+
title={`${t?.name || ''} · ${wordCount(Object.values(p.sections || {}).join(' '))} words`}
|
| 485 |
+
>
|
| 486 |
+
<Icon name={t?.icon || 'book'} size={12}/>
|
| 487 |
+
<span>{p.name}</span>
|
| 488 |
+
</button>
|
| 489 |
+
);
|
| 490 |
+
})}
|
| 491 |
+
<details className="canvas-export-menu deliverable-new-project">
|
| 492 |
+
<summary className="deliverable-project-tab"><Icon name="plus" size={12}/>New</summary>
|
| 493 |
+
<div className="canvas-export-menu-list">
|
| 494 |
+
{TEMPLATES.map(t => (
|
| 495 |
+
<button key={t.id} onClick={() => createProject(t.id)}>
|
| 496 |
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
| 497 |
+
<Icon name={t.icon} size={12}/>{t.name}
|
| 498 |
+
</span>
|
| 499 |
+
</button>
|
| 500 |
+
))}
|
| 501 |
+
</div>
|
| 502 |
+
</details>
|
| 503 |
+
</div>
|
| 504 |
+
);
|
| 505 |
|
|
|
|
| 506 |
const Header = (
|
| 507 |
<div className="page-header">
|
| 508 |
<div>
|
| 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>
|
| 520 |
</div>
|
| 521 |
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'flex-start' }}>
|
| 522 |
+
<button className="btn btn-ghost" onClick={() => setHistoryOpen(o => !o)} title="Version history">
|
| 523 |
+
<Icon name="reset" size={13}/>History · {project.versions.length}
|
| 524 |
+
</button>
|
| 525 |
<button className="btn btn-ghost" onClick={runAiPass} disabled={generatingAi} title="AI check (stub)">
|
| 526 |
{generatingAi ? <><div className="spinner"/></> : <Icon name="sparkles" size={13}/>}
|
| 527 |
AI check
|
| 528 |
</button>
|
| 529 |
+
<details className="canvas-export-menu">
|
| 530 |
+
<summary className="btn btn-primary"><Icon name="download" size={13}/>Export</summary>
|
| 531 |
+
<div className="canvas-export-menu-list">
|
| 532 |
+
<button onClick={() => exportAs('markdown')}>Markdown (.md)</button>
|
| 533 |
+
<button onClick={() => exportAs('latex')}>LaTeX (.tex)</button>
|
| 534 |
+
<button onClick={() => exportAs('html')}>HTML (.html)</button>
|
| 535 |
+
<button onClick={() => window.print()}>Print / Save as PDF</button>
|
| 536 |
+
</div>
|
| 537 |
+
</details>
|
| 538 |
+
<button className="btn btn-ghost" onClick={deleteProject} title="Delete this draft" style={{ color: 'var(--canvas-danger)' }}>
|
| 539 |
+
<Icon name="trash" size={13}/>
|
| 540 |
+
</button>
|
| 541 |
</div>
|
| 542 |
</div>
|
| 543 |
);
|
| 544 |
|
| 545 |
+
const HistoryPanel = historyOpen && (
|
| 546 |
+
<div className="deliverable-history">
|
| 547 |
+
<div className="deliverable-history-head">
|
| 548 |
+
<span>Version history · {project.versions.length}</span>
|
| 549 |
+
<button className="icon-btn" onClick={() => setHistoryOpen(false)}><Icon name="x" size={13}/></button>
|
| 550 |
+
</div>
|
| 551 |
+
{project.versions.length === 0 ? (
|
| 552 |
+
<div style={{ padding: 14, color: 'var(--canvas-text-3)', fontSize: 12 }}>
|
| 553 |
+
Snapshots auto-save every ~80 characters of edits.
|
| 554 |
+
</div>
|
| 555 |
+
) : (
|
| 556 |
+
project.versions.map((v, i) => {
|
| 557 |
+
const sec = template.sections.find(s => s.id === v.sectionId);
|
| 558 |
+
return (
|
| 559 |
+
<button key={i} className="deliverable-history-row" onClick={() => restoreVersion(v)}>
|
| 560 |
+
<span className="tag-pill">{sec?.name || v.sectionId}</span>
|
| 561 |
+
<span style={{ flex: 1 }}>{new Date(v.at).toLocaleString()}</span>
|
| 562 |
+
<Icon name="reset" size={11} style={{ color: 'var(--canvas-text-3)' }}/>
|
| 563 |
+
</button>
|
| 564 |
+
);
|
| 565 |
+
})
|
| 566 |
+
)}
|
| 567 |
+
</div>
|
| 568 |
+
);
|
| 569 |
+
|
| 570 |
const InsertPanel = (
|
| 571 |
<div className="deliverable-insertables">
|
| 572 |
+
<ArxivSearch onPick={insertIntoActive}/>
|
| 573 |
+
<div style={{ fontSize: 11, color: 'var(--canvas-text-4)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600, marginTop: 14, marginBottom: 6 }}>
|
| 574 |
+
From canvas · {localInsertables.length}
|
| 575 |
</div>
|
| 576 |
+
{localInsertables.length === 0 && (
|
| 577 |
<div style={{ padding: 12, fontSize: 11.5, color: 'var(--canvas-text-3)', background: 'var(--canvas-surface)', border: '1px dashed var(--canvas-border-2)', borderRadius: 7 }}>
|
| 578 |
+
Add a Bibliography, Highlights, Outline, or Writing widget to your canvas to surface its content here.
|
| 579 |
</div>
|
| 580 |
)}
|
| 581 |
+
{localInsertables.map((it, i) => (
|
| 582 |
<button key={i} onClick={() => insertIntoActive(it.snippet)} className="canvas-insert-row">
|
| 583 |
<span className="tag-pill">{it.kind}</span>
|
| 584 |
<span style={{ flex: 1, fontSize: 11.5, color: 'var(--canvas-text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{it.label}</span>
|
|
|
|
| 588 |
</div>
|
| 589 |
);
|
| 590 |
|
| 591 |
+
// ---------- POSTER MODE — 4-quadrant + 2 banner sections ----------
|
| 592 |
+
if (template.mode === 'poster') {
|
| 593 |
+
const layout = template.sections;
|
| 594 |
+
return (
|
| 595 |
+
<>
|
| 596 |
+
{ProjectTabs}
|
| 597 |
+
{Header}
|
| 598 |
+
{HistoryPanel}
|
| 599 |
+
<div className="poster-grid">
|
| 600 |
+
<div className="poster-banner">
|
| 601 |
+
<PosterPanel section={layout[0]} sections={sections} updateSection={updateSection}/>
|
| 602 |
+
</div>
|
| 603 |
+
<PosterPanel section={layout[1]} sections={sections} updateSection={updateSection}/>
|
| 604 |
+
<PosterPanel section={layout[2]} sections={sections} updateSection={updateSection}/>
|
| 605 |
+
<PosterPanel section={layout[3]} sections={sections} updateSection={updateSection}/>
|
| 606 |
+
<PosterPanel section={layout[4]} sections={sections} updateSection={updateSection}/>
|
| 607 |
+
<div className="poster-banner">
|
| 608 |
+
<PosterPanel section={layout[5]} sections={sections} updateSection={updateSection}/>
|
| 609 |
+
</div>
|
| 610 |
+
</div>
|
| 611 |
+
<div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end' }}>
|
| 612 |
+
{InsertPanel}
|
| 613 |
+
</div>
|
| 614 |
+
</>
|
| 615 |
+
);
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
// ---------- SLIDES MODE — Google Slides feel ----------
|
| 619 |
if (template.mode === 'slides') {
|
| 620 |
const activeIdx = template.sections.findIndex(s => s.id === activeSectionId);
|
| 621 |
const active = template.sections[activeIdx] || template.sections[0];
|
| 622 |
const text = sections[active.id] || '';
|
| 623 |
const aiForSlide = aiNotes && aiNotes.find(n => n.sectionId === active.id);
|
|
|
|
| 624 |
const lines = text.split(/\n+/).map(l => l.trim()).filter(Boolean);
|
| 625 |
|
| 626 |
return (
|
| 627 |
<>
|
| 628 |
+
{ProjectTabs}
|
| 629 |
{Header}
|
| 630 |
+
{HistoryPanel}
|
| 631 |
<div className="deliverable-slides-grid">
|
|
|
|
| 632 |
<div className="slide-thumbs">
|
| 633 |
<div style={{ fontSize: 10, color: 'var(--canvas-text-4)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600, padding: '0 4px 6px' }}>
|
| 634 |
{template.sections.length} slides
|
|
|
|
| 652 |
);
|
| 653 |
})}
|
| 654 |
</div>
|
|
|
|
|
|
|
| 655 |
<div>
|
| 656 |
<div className="slide-canvas-wrap">
|
| 657 |
<div className="slide-canvas">
|
|
|
|
| 668 |
<div className="slide-canvas-footer">{activeIdx + 1} / {template.sections.length}</div>
|
| 669 |
</div>
|
| 670 |
</div>
|
|
|
|
|
|
|
| 671 |
<div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
| 672 |
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
| 673 |
<span style={{ fontSize: 11, color: 'var(--canvas-text-4)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600 }}>
|
|
|
|
| 679 |
{wordCount(text)}{active.target ? `/${active.target}` : ''} words
|
| 680 |
</span>
|
| 681 |
</div>
|
| 682 |
+
<SlashTextarea
|
|
|
|
| 683 |
value={text}
|
| 684 |
+
onChange={(v) => updateSection(active.id, v)}
|
| 685 |
placeholder={active.hint}
|
| 686 |
+
rows={6}
|
| 687 |
/>
|
| 688 |
{(active.checks || []).length > 0 && (
|
| 689 |
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
|
|
|
| 701 |
</div>
|
| 702 |
)}
|
| 703 |
{aiForSlide && (
|
| 704 |
+
<div className="notion-callout">
|
| 705 |
+
<Icon name="sparkles" size={14}/>
|
| 706 |
+
<div>
|
| 707 |
+
<div className="notion-callout-label">AI suggestion · stub</div>
|
| 708 |
+
<div>{aiForSlide.msg}</div>
|
| 709 |
+
</div>
|
| 710 |
</div>
|
| 711 |
)}
|
| 712 |
</div>
|
| 713 |
</div>
|
|
|
|
| 714 |
{InsertPanel}
|
| 715 |
</div>
|
| 716 |
</>
|
| 717 |
);
|
| 718 |
}
|
| 719 |
|
| 720 |
+
// ---------- PAPER / DOCUMENT MODE — Notion single-surface page ----------
|
| 721 |
const paperLike = template.mode === 'paper';
|
| 722 |
return (
|
| 723 |
<>
|
| 724 |
+
{ProjectTabs}
|
| 725 |
{Header}
|
| 726 |
+
{HistoryPanel}
|
| 727 |
<div className="notion-deliverable-grid">
|
|
|
|
| 728 |
<div className="notion-toc">
|
| 729 |
<div className="notion-toc-label">On this page</div>
|
| 730 |
{template.sections.map(s => {
|
|
|
|
| 744 |
})}
|
| 745 |
</div>
|
| 746 |
|
|
|
|
| 747 |
<div className={`notion-page-wrap ${paperLike ? 'paper' : ''}`}>
|
| 748 |
<div className={`notion-page ${paperLike ? 'serif' : ''}`}>
|
| 749 |
+
<h1 className="notion-page-title">{project.name}</h1>
|
| 750 |
<div className="notion-page-meta">
|
| 751 |
+
{totalWords} words · ~{readingMinutes(totalWords)} min read · {template.sections.length} sections{paperLike ? ' · academic paper' : ''}
|
| 752 |
</div>
|
| 753 |
{template.sections.map(s => {
|
| 754 |
const text = sections[s.id] || '';
|
| 755 |
const aiForSection = aiNotes ? aiNotes.find(n => n.sectionId === s.id) : null;
|
|
|
|
| 756 |
return (
|
| 757 |
<div key={s.id} id={`notion-section-${s.id}`} className="notion-block">
|
| 758 |
<h2 className={`notion-h2 ${paperLike ? 'serif' : ''}`}>{s.name}</h2>
|
| 759 |
+
<RichBlock
|
|
|
|
| 760 |
value={text}
|
| 761 |
onChange={(v) => updateSection(s.id, v)}
|
| 762 |
placeholder={`Start writing ${s.name.toLowerCase()}…`}
|
| 763 |
+
hint={s.hint}
|
| 764 |
serif={paperLike}
|
| 765 |
/>
|
| 766 |
{(s.checks || []).length > 0 && (
|
|
|
|
| 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
|
| 810 |
+
// • Click: switches to a textarea source view
|
| 811 |
+
// • While editing: floating toolbar above with Bold / Italic / H2 / list / link / cite / math
|
| 812 |
+
// • Slash commands still work via the underlying textarea
|
| 813 |
+
// ============================================================================
|
| 814 |
+
const wrap = (text, l, r = l) => `${l}${text || 'text'}${r}`;
|
| 815 |
+
const lineWrap = (text, prefix) =>
|
| 816 |
+
(text ? text.split('\n').map(line => line ? `${prefix}${line}` : line).join('\n') : `${prefix}`);
|
| 817 |
+
|
| 818 |
+
const TOOLBAR = [
|
| 819 |
+
{ id: 'bold', icon: 'pencil', label: 'Bold (⌘B)', run: (sel) => wrap(sel, '**') },
|
| 820 |
+
{ id: 'italic', icon: 'pencil', label: 'Italic (⌘I)', run: (sel) => wrap(sel, '*') },
|
| 821 |
+
{ id: 'code', icon: 'flask', label: 'Inline code', run: (sel) => wrap(sel, '`') },
|
| 822 |
+
{ id: 'h2', icon: 'list', label: 'Heading', run: (sel) => `## ${sel || 'Heading'}` },
|
| 823 |
+
{ id: 'h3', icon: 'list', label: 'Subheading', run: (sel) => `### ${sel || 'Subheading'}` },
|
| 824 |
+
{ id: 'list', icon: 'list', label: 'Bullet list', run: (sel) => lineWrap(sel, '- ') },
|
| 825 |
+
{ id: 'numbered', icon: 'list', label: 'Numbered list', run: (sel) => lineWrap(sel, '1. ') },
|
| 826 |
+
{ id: 'quote', icon: 'cite', label: 'Block quote', run: (sel) => lineWrap(sel, '> ') },
|
| 827 |
+
{ id: 'link', icon: 'link', label: 'Link', run: (sel) => `[${sel || 'link text'}](https://)` },
|
| 828 |
+
{ id: 'cite', icon: 'book', label: 'Citation @key', run: (sel) => `(@${sel || 'key'})` },
|
| 829 |
+
{ id: 'math', icon: 'flask', label: 'Inline math (LaTeX)', run: (sel) => `$${sel || 'x^2'}$` },
|
| 830 |
+
{ id: 'math-block', icon: 'flask', label: 'Math block', run: (sel) => `\n$$\n${sel || 'E = mc^2'}\n$$\n` },
|
| 831 |
+
];
|
| 832 |
+
|
| 833 |
+
function RichBlock({ value, onChange, placeholder, serif, hint }) {
|
| 834 |
+
const [editing, setEditing] = useState(false);
|
| 835 |
+
const [slash, setSlash] = useState(null);
|
| 836 |
+
const taRef = useRef(null);
|
| 837 |
+
const containerRef = useRef(null);
|
| 838 |
+
|
| 839 |
+
// Auto-grow when editing
|
| 840 |
+
useEffect(() => {
|
| 841 |
+
if (!editing || !taRef.current) return;
|
| 842 |
+
taRef.current.style.height = 'auto';
|
| 843 |
+
taRef.current.style.height = taRef.current.scrollHeight + 'px';
|
| 844 |
+
}, [value, editing]);
|
| 845 |
+
|
| 846 |
+
// Click outside to leave edit mode
|
| 847 |
+
useEffect(() => {
|
| 848 |
+
if (!editing) return;
|
| 849 |
+
const onDocClick = (e) => {
|
| 850 |
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
| 851 |
+
setEditing(false);
|
| 852 |
+
setSlash(null);
|
| 853 |
+
}
|
| 854 |
+
};
|
| 855 |
+
document.addEventListener('mousedown', onDocClick);
|
| 856 |
+
return () => document.removeEventListener('mousedown', onDocClick);
|
| 857 |
+
}, [editing]);
|
| 858 |
+
|
| 859 |
+
const applyToolbar = (cmd) => {
|
| 860 |
+
const ta = taRef.current;
|
| 861 |
+
if (!ta) return;
|
| 862 |
+
const { selectionStart: s, selectionEnd: e } = ta;
|
| 863 |
+
const before = value.slice(0, s);
|
| 864 |
+
const sel = value.slice(s, e);
|
| 865 |
+
const after = value.slice(e);
|
| 866 |
+
const replacement = cmd.run(sel);
|
| 867 |
+
const next = before + replacement + after;
|
| 868 |
+
onChange(next);
|
| 869 |
+
// Reposition cursor at end of inserted text
|
| 870 |
+
setTimeout(() => {
|
| 871 |
+
if (taRef.current) {
|
| 872 |
+
const pos = before.length + replacement.length;
|
| 873 |
+
taRef.current.setSelectionRange(pos, pos);
|
| 874 |
+
taRef.current.focus();
|
| 875 |
+
}
|
| 876 |
+
}, 0);
|
| 877 |
+
};
|
| 878 |
+
|
| 879 |
+
const onTextChange = (e) => {
|
| 880 |
+
const val = e.target.value;
|
| 881 |
+
const cursor = e.target.selectionStart;
|
| 882 |
+
onChange(val);
|
| 883 |
+
const before = val.slice(0, cursor);
|
| 884 |
+
const m = before.match(/(?:^|\n)(\/[\w-]*)$/);
|
| 885 |
+
if (m) {
|
| 886 |
+
const q = m[1].slice(1).toLowerCase();
|
| 887 |
+
const choices = SLASH_COMMANDS.filter(c => c.label.toLowerCase().includes(q) || c.id.includes(q)).slice(0, 8);
|
| 888 |
+
setSlash({ start: cursor - m[1].length, query: q, choices, idx: 0 });
|
| 889 |
+
} else {
|
| 890 |
+
setSlash(null);
|
| 891 |
+
}
|
| 892 |
+
};
|
| 893 |
+
|
| 894 |
+
const insertSlash = (cmd) => {
|
| 895 |
+
if (!slash) return;
|
| 896 |
+
const before = value.slice(0, slash.start);
|
| 897 |
+
const after = value.slice(slash.start + 1 + slash.query.length);
|
| 898 |
+
const inserted = cmd.insert();
|
| 899 |
+
onChange(before + inserted + after);
|
| 900 |
+
setSlash(null);
|
| 901 |
+
setTimeout(() => {
|
| 902 |
+
if (taRef.current) {
|
| 903 |
+
const pos = before.length + inserted.length;
|
| 904 |
+
taRef.current.setSelectionRange(pos, pos);
|
| 905 |
+
taRef.current.focus();
|
| 906 |
+
}
|
| 907 |
+
}, 0);
|
| 908 |
+
};
|
| 909 |
+
|
| 910 |
+
const onKeyDown = (e) => {
|
| 911 |
+
// ⌘B / ⌘I keyboard shortcuts (Word/Docs convention)
|
| 912 |
+
if ((e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
| 913 |
+
if (e.key.toLowerCase() === 'b') { e.preventDefault(); applyToolbar(TOOLBAR.find(t => t.id === 'bold')); return; }
|
| 914 |
+
if (e.key.toLowerCase() === 'i') { e.preventDefault(); applyToolbar(TOOLBAR.find(t => t.id === 'italic')); return; }
|
| 915 |
+
}
|
| 916 |
+
if (slash && slash.choices.length > 0) {
|
| 917 |
+
if (e.key === 'ArrowDown') { e.preventDefault(); setSlash(s => ({ ...s, idx: Math.min(s.choices.length - 1, s.idx + 1) })); }
|
| 918 |
+
else if (e.key === 'ArrowUp') { e.preventDefault(); setSlash(s => ({ ...s, idx: Math.max(0, s.idx - 1) })); }
|
| 919 |
+
else if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); insertSlash(slash.choices[slash.idx]); }
|
| 920 |
+
else if (e.key === 'Escape') setSlash(null);
|
| 921 |
+
}
|
| 922 |
+
};
|
| 923 |
+
|
| 924 |
+
return (
|
| 925 |
+
<div ref={containerRef} className="rich-block">
|
| 926 |
+
{/* Floating toolbar — only when actively editing */}
|
| 927 |
+
{editing && (
|
| 928 |
+
<div className="rich-toolbar" onMouseDown={(e) => e.preventDefault() /* keep textarea focused */}>
|
| 929 |
+
{TOOLBAR.map(t => (
|
| 930 |
+
<button key={t.id} title={t.label} onClick={() => applyToolbar(t)}>
|
| 931 |
+
{t.id === 'bold' && <strong>B</strong>}
|
| 932 |
+
{t.id === 'italic' && <em>I</em>}
|
| 933 |
+
{t.id === 'code' && <span style={{ fontFamily: 'var(--canvas-mono)', fontSize: 11 }}>{'<>'}</span>}
|
| 934 |
+
{t.id === 'h2' && <span style={{ fontWeight: 700 }}>H2</span>}
|
| 935 |
+
{t.id === 'h3' && <span style={{ fontWeight: 700, fontSize: 11 }}>H3</span>}
|
| 936 |
+
{t.id === 'list' && '•'}
|
| 937 |
+
{t.id === 'numbered' && <span style={{ fontFamily: 'var(--canvas-mono)', fontSize: 11 }}>1.</span>}
|
| 938 |
+
{t.id === 'quote' && '"'}
|
| 939 |
+
{t.id === 'link' && <Icon name="link" size={12}/>}
|
| 940 |
+
{t.id === 'cite' && '@'}
|
| 941 |
+
{t.id === 'math' && <span style={{ fontFamily: 'serif', fontStyle: 'italic' }}>x</span>}
|
| 942 |
+
{t.id === 'math-block' && <span style={{ fontFamily: 'serif' }}>∑</span>}
|
| 943 |
+
</button>
|
| 944 |
+
))}
|
| 945 |
+
</div>
|
| 946 |
+
)}
|
| 947 |
+
|
| 948 |
+
{editing ? (
|
| 949 |
+
<div style={{ position: 'relative' }}>
|
| 950 |
+
<textarea
|
| 951 |
+
ref={taRef}
|
| 952 |
+
className={`notion-text ${serif ? 'serif' : ''}`}
|
| 953 |
+
value={value}
|
| 954 |
+
onChange={onTextChange}
|
| 955 |
+
onKeyDown={onKeyDown}
|
| 956 |
+
placeholder={placeholder}
|
| 957 |
+
autoFocus
|
| 958 |
+
rows={1}
|
| 959 |
+
/>
|
| 960 |
+
{slash && slash.choices.length > 0 && (
|
| 961 |
+
<div className="slash-menu">
|
| 962 |
+
<div className="slash-menu-head">Insert block</div>
|
| 963 |
+
{slash.choices.map((c, i) => (
|
| 964 |
+
<button key={c.id}
|
| 965 |
+
onMouseEnter={() => setSlash(s => ({ ...s, idx: i }))}
|
| 966 |
+
onClick={() => insertSlash(c)}
|
| 967 |
+
className={i === slash.idx ? 'active' : ''}>
|
| 968 |
+
<Icon name={c.icon} size={13}/>
|
| 969 |
+
<span style={{ flex: 1 }}>{c.label}</span>
|
| 970 |
+
<span className="slash-menu-kind">{c.kind}</span>
|
| 971 |
+
</button>
|
| 972 |
+
))}
|
| 973 |
+
</div>
|
| 974 |
+
)}
|
| 975 |
+
</div>
|
| 976 |
+
) : (
|
| 977 |
+
<div
|
| 978 |
+
className={`rich-rendered ${serif ? 'serif' : ''}`}
|
| 979 |
+
onClick={() => setEditing(true)}
|
| 980 |
+
onFocus={() => setEditing(true)}
|
| 981 |
+
tabIndex={0}
|
| 982 |
+
>
|
| 983 |
+
{value ? (
|
| 984 |
+
<ReactMarkdown remarkPlugins={REMARK_PLUGINS} rehypePlugins={REHYPE_PLUGINS}>{value}</ReactMarkdown>
|
| 985 |
+
) : (
|
| 986 |
+
<span className="rich-placeholder">{hint || placeholder || 'Click to edit'}</span>
|
| 987 |
+
)}
|
| 988 |
+
</div>
|
| 989 |
+
)}
|
| 990 |
+
</div>
|
| 991 |
+
);
|
| 992 |
+
}
|
| 993 |
+
|
| 994 |
+
// ============================================================================
|
| 995 |
+
// SlashTextarea — auto-grows; `/` opens a block-insert popover.
|
| 996 |
+
// ============================================================================
|
| 997 |
+
function SlashTextarea({ value, onChange, placeholder, serif, notion, rows = 1 }) {
|
| 998 |
const ref = useRef(null);
|
| 999 |
+
const [slash, setSlash] = useState(null); // { start, query, choices, idx }
|
| 1000 |
+
|
| 1001 |
useEffect(() => {
|
| 1002 |
+
if (!ref.current || !notion) return;
|
| 1003 |
ref.current.style.height = 'auto';
|
| 1004 |
ref.current.style.height = ref.current.scrollHeight + 'px';
|
| 1005 |
+
}, [value, notion]);
|
| 1006 |
+
|
| 1007 |
+
const onTextChange = (e) => {
|
| 1008 |
+
const val = e.target.value;
|
| 1009 |
+
const cursor = e.target.selectionStart;
|
| 1010 |
+
onChange(val);
|
| 1011 |
+
const before = val.slice(0, cursor);
|
| 1012 |
+
const m = before.match(/(?:^|\n)(\/[\w-]*)$/);
|
| 1013 |
+
if (m) {
|
| 1014 |
+
const q = m[1].slice(1).toLowerCase();
|
| 1015 |
+
const choices = SLASH_COMMANDS.filter(c => c.label.toLowerCase().includes(q) || c.id.includes(q)).slice(0, 8);
|
| 1016 |
+
setSlash({ start: cursor - m[1].length, query: q, choices, idx: 0 });
|
| 1017 |
+
} else {
|
| 1018 |
+
setSlash(null);
|
| 1019 |
+
}
|
| 1020 |
+
};
|
| 1021 |
+
|
| 1022 |
+
const insertCmd = (cmd) => {
|
| 1023 |
+
if (!slash) return;
|
| 1024 |
+
const before = value.slice(0, slash.start);
|
| 1025 |
+
const after = value.slice(slash.start + 1 + slash.query.length);
|
| 1026 |
+
const inserted = cmd.insert();
|
| 1027 |
+
onChange(before + inserted + after);
|
| 1028 |
+
setSlash(null);
|
| 1029 |
+
setTimeout(() => {
|
| 1030 |
+
if (ref.current) {
|
| 1031 |
+
const pos = before.length + inserted.length;
|
| 1032 |
+
ref.current.setSelectionRange(pos, pos);
|
| 1033 |
+
ref.current.focus();
|
| 1034 |
+
}
|
| 1035 |
+
}, 0);
|
| 1036 |
+
};
|
| 1037 |
+
|
| 1038 |
+
const onKeyDown = (e) => {
|
| 1039 |
+
if (!slash || slash.choices.length === 0) return;
|
| 1040 |
+
if (e.key === 'ArrowDown') { e.preventDefault(); setSlash(s => ({ ...s, idx: Math.min(s.choices.length - 1, s.idx + 1) })); }
|
| 1041 |
+
if (e.key === 'ArrowUp') { e.preventDefault(); setSlash(s => ({ ...s, idx: Math.max(0, s.idx - 1) })); }
|
| 1042 |
+
if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); insertCmd(slash.choices[slash.idx]); }
|
| 1043 |
+
if (e.key === 'Escape') setSlash(null);
|
| 1044 |
+
};
|
| 1045 |
+
|
| 1046 |
+
return (
|
| 1047 |
+
<div style={{ position: 'relative' }}>
|
| 1048 |
+
<textarea
|
| 1049 |
+
ref={ref}
|
| 1050 |
+
className={notion ? `notion-text ${serif ? 'serif' : ''}` : 'textarea'}
|
| 1051 |
+
value={value}
|
| 1052 |
+
onChange={onTextChange}
|
| 1053 |
+
onKeyDown={onKeyDown}
|
| 1054 |
+
placeholder={placeholder}
|
| 1055 |
+
rows={rows}
|
| 1056 |
+
style={notion ? undefined : { minHeight: 110, fontFamily: 'var(--canvas-sans)', fontSize: 13, lineHeight: 1.6 }}
|
| 1057 |
+
/>
|
| 1058 |
+
{slash && slash.choices.length > 0 && (
|
| 1059 |
+
<div className="slash-menu">
|
| 1060 |
+
<div className="slash-menu-head">Insert block</div>
|
| 1061 |
+
{slash.choices.map((c, i) => (
|
| 1062 |
+
<button key={c.id}
|
| 1063 |
+
onMouseEnter={() => setSlash(s => ({ ...s, idx: i }))}
|
| 1064 |
+
onClick={() => insertCmd(c)}
|
| 1065 |
+
className={i === slash.idx ? 'active' : ''}>
|
| 1066 |
+
<Icon name={c.icon} size={13}/>
|
| 1067 |
+
<span style={{ flex: 1 }}>{c.label}</span>
|
| 1068 |
+
<span className="slash-menu-kind">{c.kind}</span>
|
| 1069 |
+
</button>
|
| 1070 |
+
))}
|
| 1071 |
+
</div>
|
| 1072 |
+
)}
|
| 1073 |
+
</div>
|
| 1074 |
+
);
|
| 1075 |
+
}
|
| 1076 |
+
|
| 1077 |
+
// ============================================================================
|
| 1078 |
+
// Poster panel — single-section block in the poster layout
|
| 1079 |
+
// ============================================================================
|
| 1080 |
+
function PosterPanel({ section, sections, updateSection }) {
|
| 1081 |
+
if (!section) return null;
|
| 1082 |
+
const text = sections[section.id] || '';
|
| 1083 |
+
return (
|
| 1084 |
+
<div className="poster-panel">
|
| 1085 |
+
<div className="poster-panel-head">{section.name}</div>
|
| 1086 |
+
<RichBlock
|
| 1087 |
+
value={text}
|
| 1088 |
+
onChange={(v) => updateSection(section.id, v)}
|
| 1089 |
+
placeholder={section.hint}
|
| 1090 |
+
hint={section.hint}
|
| 1091 |
+
/>
|
| 1092 |
+
</div>
|
| 1093 |
+
);
|
| 1094 |
+
}
|
| 1095 |
+
|
| 1096 |
+
// ============================================================================
|
| 1097 |
+
// arXiv search — public ATOM API, CORS-enabled.
|
| 1098 |
+
// ============================================================================
|
| 1099 |
+
function ArxivSearch({ onPick }) {
|
| 1100 |
+
const [q, setQ] = useState('');
|
| 1101 |
+
const [busy, setBusy] = useState(false);
|
| 1102 |
+
const [results, setResults] = useState([]);
|
| 1103 |
+
|
| 1104 |
+
const search = async () => {
|
| 1105 |
+
if (!q.trim()) return;
|
| 1106 |
+
setBusy(true);
|
| 1107 |
+
try {
|
| 1108 |
+
const url = `https://export.arxiv.org/api/query?search_query=all:${encodeURIComponent(q)}&max_results=5`;
|
| 1109 |
+
const res = await fetch(url);
|
| 1110 |
+
const xml = await res.text();
|
| 1111 |
+
const doc = new DOMParser().parseFromString(xml, 'text/xml');
|
| 1112 |
+
const entries = Array.from(doc.getElementsByTagName('entry')).map(e => {
|
| 1113 |
+
const id = e.getElementsByTagName('id')[0]?.textContent?.split('/').pop() || '';
|
| 1114 |
+
const title = e.getElementsByTagName('title')[0]?.textContent?.trim() || '';
|
| 1115 |
+
const authors = Array.from(e.getElementsByTagName('author')).map(a => a.getElementsByTagName('name')[0]?.textContent?.trim()).filter(Boolean);
|
| 1116 |
+
const year = (e.getElementsByTagName('published')[0]?.textContent || '').slice(0, 4);
|
| 1117 |
+
return { id, title, authors, year };
|
| 1118 |
+
});
|
| 1119 |
+
setResults(entries);
|
| 1120 |
+
} catch (e) {
|
| 1121 |
+
fireToast('arXiv search failed', 'danger');
|
| 1122 |
+
} finally {
|
| 1123 |
+
setBusy(false);
|
| 1124 |
+
}
|
| 1125 |
+
};
|
| 1126 |
+
|
| 1127 |
return (
|
| 1128 |
+
<div>
|
| 1129 |
+
<div style={{ fontSize: 11, color: 'var(--canvas-text-4)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600, marginBottom: 6 }}>
|
| 1130 |
+
Search arXiv
|
| 1131 |
+
</div>
|
| 1132 |
+
<div style={{ display: 'flex', gap: 4 }}>
|
| 1133 |
+
<input className="input" placeholder="Predictive coding…" value={q}
|
| 1134 |
+
onChange={e => setQ(e.target.value)}
|
| 1135 |
+
onKeyDown={e => { if (e.key === 'Enter') search(); }}
|
| 1136 |
+
style={{ fontSize: 12, padding: '5px 8px' }}/>
|
| 1137 |
+
<button className="icon-btn" onClick={search} disabled={!q.trim() || busy} title="Search arXiv">
|
| 1138 |
+
{busy ? <div className="spinner"/> : <Icon name="search" size={13}/>}
|
| 1139 |
+
</button>
|
| 1140 |
+
</div>
|
| 1141 |
+
{results.length > 0 && (
|
| 1142 |
+
<div style={{ marginTop: 6, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
| 1143 |
+
{results.map(r => {
|
| 1144 |
+
const a = r.authors[0] ? r.authors[0].split(' ').pop() : 'Unknown';
|
| 1145 |
+
const snippet = ` (${a}, ${r.year}; arXiv:${r.id})`;
|
| 1146 |
+
return (
|
| 1147 |
+
<button key={r.id} className="canvas-insert-row" onClick={() => onPick(snippet)}>
|
| 1148 |
+
<span className="tag-pill">arXiv</span>
|
| 1149 |
+
<span style={{ flex: 1, fontSize: 11.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={r.title}>{r.title}</span>
|
| 1150 |
+
<Icon name="plus" size={12} style={{ color: 'var(--canvas-text-3)' }}/>
|
| 1151 |
+
</button>
|
| 1152 |
+
);
|
| 1153 |
+
})}
|
| 1154 |
+
</div>
|
| 1155 |
+
)}
|
| 1156 |
+
</div>
|
| 1157 |
);
|
| 1158 |
}
|
| 1159 |
|
phd-advisor-frontend/src/styles/CanvasPage.css
CHANGED
|
@@ -1609,6 +1609,374 @@ 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 |
/* ----- Notion-style single-surface deliverable editor ----- */
|
| 1613 |
.canvas-page-with-sidebar .notion-deliverable-grid {
|
| 1614 |
display: grid;
|
|
|
|
| 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;
|
| 1615 |
+
gap: 4px;
|
| 1616 |
+
flex-wrap: wrap;
|
| 1617 |
+
align-items: center;
|
| 1618 |
+
margin-bottom: 14px;
|
| 1619 |
+
padding-bottom: 12px;
|
| 1620 |
+
border-bottom: 1px solid var(--canvas-border);
|
| 1621 |
+
}
|
| 1622 |
+
.canvas-page-with-sidebar .deliverable-project-tab {
|
| 1623 |
+
display: inline-flex;
|
| 1624 |
+
align-items: center;
|
| 1625 |
+
gap: 6px;
|
| 1626 |
+
padding: 6px 11px;
|
| 1627 |
+
border-radius: 6px;
|
| 1628 |
+
background: transparent;
|
| 1629 |
+
border: 1px solid transparent;
|
| 1630 |
+
color: var(--canvas-text-2);
|
| 1631 |
+
font-family: inherit;
|
| 1632 |
+
font-size: 12.5px;
|
| 1633 |
+
cursor: pointer;
|
| 1634 |
+
transition: background .12s, color .12s, border-color .12s;
|
| 1635 |
+
}
|
| 1636 |
+
.canvas-page-with-sidebar .deliverable-project-tab:hover {
|
| 1637 |
+
background: var(--canvas-surface-2);
|
| 1638 |
+
color: var(--canvas-text);
|
| 1639 |
+
}
|
| 1640 |
+
.canvas-page-with-sidebar .deliverable-project-tab.active {
|
| 1641 |
+
background: var(--canvas-surface-2);
|
| 1642 |
+
border-color: var(--canvas-border);
|
| 1643 |
+
color: var(--canvas-text);
|
| 1644 |
+
font-weight: 600;
|
| 1645 |
+
}
|
| 1646 |
+
.canvas-page-with-sidebar .deliverable-new-project { position: relative; }
|
| 1647 |
+
.canvas-page-with-sidebar .deliverable-new-project summary {
|
| 1648 |
+
list-style: none;
|
| 1649 |
+
cursor: pointer;
|
| 1650 |
+
color: var(--canvas-text-3);
|
| 1651 |
+
}
|
| 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;
|
| 1658 |
+
border: none;
|
| 1659 |
+
outline: none;
|
| 1660 |
+
width: 100%;
|
| 1661 |
+
font-family: inherit;
|
| 1662 |
+
font-size: 28px;
|
| 1663 |
+
font-weight: 700;
|
| 1664 |
+
letter-spacing: -0.02em;
|
| 1665 |
+
margin: 0;
|
| 1666 |
+
padding: 2px 0;
|
| 1667 |
+
color: var(--canvas-text);
|
| 1668 |
+
}
|
| 1669 |
+
.canvas-page-with-sidebar .page-title-editable:focus {
|
| 1670 |
+
background: rgba(255, 255, 255, 0.03);
|
| 1671 |
+
border-radius: 4px;
|
| 1672 |
+
padding: 2px 6px;
|
| 1673 |
+
margin: 0 -6px;
|
| 1674 |
+
}
|
| 1675 |
+
.canvas-page-with-sidebar[data-canvas-theme="light"] .page-title-editable:focus {
|
| 1676 |
+
background: rgba(0, 0, 0, 0.025);
|
| 1677 |
+
}
|
| 1678 |
+
|
| 1679 |
+
/* Version history dropdown panel */
|
| 1680 |
+
.canvas-page-with-sidebar .deliverable-history {
|
| 1681 |
+
background: var(--canvas-surface);
|
| 1682 |
+
border: 1px solid var(--canvas-border);
|
| 1683 |
+
border-radius: 8px;
|
| 1684 |
+
margin-bottom: 16px;
|
| 1685 |
+
padding: 6px;
|
| 1686 |
+
max-height: 320px;
|
| 1687 |
+
overflow-y: auto;
|
| 1688 |
+
}
|
| 1689 |
+
.canvas-page-with-sidebar .deliverable-history-head {
|
| 1690 |
+
display: flex;
|
| 1691 |
+
align-items: center;
|
| 1692 |
+
justify-content: space-between;
|
| 1693 |
+
padding: 4px 8px 8px;
|
| 1694 |
+
font-size: 11px;
|
| 1695 |
+
font-family: var(--canvas-mono);
|
| 1696 |
+
text-transform: uppercase;
|
| 1697 |
+
letter-spacing: 0.08em;
|
| 1698 |
+
color: var(--canvas-text-3);
|
| 1699 |
+
border-bottom: 1px solid var(--canvas-border);
|
| 1700 |
+
margin-bottom: 4px;
|
| 1701 |
+
}
|
| 1702 |
+
.canvas-page-with-sidebar .deliverable-history-row {
|
| 1703 |
+
display: flex;
|
| 1704 |
+
align-items: center;
|
| 1705 |
+
gap: 8px;
|
| 1706 |
+
width: 100%;
|
| 1707 |
+
padding: 6px 8px;
|
| 1708 |
+
background: transparent;
|
| 1709 |
+
border: none;
|
| 1710 |
+
border-radius: 4px;
|
| 1711 |
+
color: var(--canvas-text-2);
|
| 1712 |
+
font-family: inherit;
|
| 1713 |
+
font-size: 12px;
|
| 1714 |
+
cursor: pointer;
|
| 1715 |
+
}
|
| 1716 |
+
.canvas-page-with-sidebar .deliverable-history-row:hover {
|
| 1717 |
+
background: var(--canvas-surface-2);
|
| 1718 |
+
color: var(--canvas-text);
|
| 1719 |
+
}
|
| 1720 |
+
|
| 1721 |
+
/* Slash menu (block-insert popover) */
|
| 1722 |
+
.canvas-page-with-sidebar .slash-menu {
|
| 1723 |
+
position: absolute;
|
| 1724 |
+
bottom: calc(100% + 4px);
|
| 1725 |
+
left: 0;
|
| 1726 |
+
z-index: 30;
|
| 1727 |
+
background: var(--canvas-surface);
|
| 1728 |
+
border: 1px solid var(--canvas-border-2);
|
| 1729 |
+
border-radius: 8px;
|
| 1730 |
+
box-shadow: var(--canvas-shadow-lg);
|
| 1731 |
+
padding: 4px;
|
| 1732 |
+
min-width: 240px;
|
| 1733 |
+
max-height: 280px;
|
| 1734 |
+
overflow-y: auto;
|
| 1735 |
+
display: flex;
|
| 1736 |
+
flex-direction: column;
|
| 1737 |
+
}
|
| 1738 |
+
.canvas-page-with-sidebar .slash-menu-head {
|
| 1739 |
+
font-size: 10px;
|
| 1740 |
+
font-family: var(--canvas-mono);
|
| 1741 |
+
text-transform: uppercase;
|
| 1742 |
+
letter-spacing: 0.08em;
|
| 1743 |
+
color: var(--canvas-text-4);
|
| 1744 |
+
padding: 6px 8px 4px;
|
| 1745 |
+
}
|
| 1746 |
+
.canvas-page-with-sidebar .slash-menu button {
|
| 1747 |
+
display: flex;
|
| 1748 |
+
align-items: center;
|
| 1749 |
+
gap: 8px;
|
| 1750 |
+
background: transparent;
|
| 1751 |
+
border: none;
|
| 1752 |
+
padding: 6px 8px;
|
| 1753 |
+
border-radius: 4px;
|
| 1754 |
+
font-family: inherit;
|
| 1755 |
+
font-size: 12.5px;
|
| 1756 |
+
color: var(--canvas-text);
|
| 1757 |
+
text-align: left;
|
| 1758 |
+
cursor: pointer;
|
| 1759 |
+
}
|
| 1760 |
+
.canvas-page-with-sidebar .slash-menu button:hover,
|
| 1761 |
+
.canvas-page-with-sidebar .slash-menu button.active {
|
| 1762 |
+
background: var(--canvas-surface-2);
|
| 1763 |
+
}
|
| 1764 |
+
.canvas-page-with-sidebar .slash-menu-kind {
|
| 1765 |
+
font-family: var(--canvas-mono);
|
| 1766 |
+
font-size: 9.5px;
|
| 1767 |
+
color: var(--canvas-text-4);
|
| 1768 |
+
text-transform: uppercase;
|
| 1769 |
+
letter-spacing: 0.06em;
|
| 1770 |
+
}
|
| 1771 |
+
|
| 1772 |
+
/* Poster mode — 4-quadrant grid, banners on top and bottom */
|
| 1773 |
+
.canvas-page-with-sidebar .poster-grid {
|
| 1774 |
+
display: grid;
|
| 1775 |
+
grid-template-columns: 1fr 1fr;
|
| 1776 |
+
gap: 14px;
|
| 1777 |
+
background: var(--canvas-surface);
|
| 1778 |
+
border: 1px solid var(--canvas-border);
|
| 1779 |
+
border-radius: 10px;
|
| 1780 |
+
padding: 18px;
|
| 1781 |
+
}
|
| 1782 |
+
.canvas-page-with-sidebar .poster-banner { grid-column: 1 / -1; }
|
| 1783 |
+
.canvas-page-with-sidebar .poster-panel {
|
| 1784 |
+
background: var(--canvas-bg-2);
|
| 1785 |
+
border: 1px solid var(--canvas-border);
|
| 1786 |
+
border-radius: 8px;
|
| 1787 |
+
padding: 14px;
|
| 1788 |
+
display: flex;
|
| 1789 |
+
flex-direction: column;
|
| 1790 |
+
gap: 8px;
|
| 1791 |
+
}
|
| 1792 |
+
.canvas-page-with-sidebar[data-canvas-theme="light"] .poster-panel {
|
| 1793 |
+
background: #ffffff;
|
| 1794 |
+
border-color: rgba(15, 15, 15, 0.08);
|
| 1795 |
+
}
|
| 1796 |
+
.canvas-page-with-sidebar .poster-panel-head {
|
| 1797 |
+
font-size: 13px;
|
| 1798 |
+
font-weight: 700;
|
| 1799 |
+
text-transform: uppercase;
|
| 1800 |
+
letter-spacing: 0.06em;
|
| 1801 |
+
color: var(--canvas-accent);
|
| 1802 |
+
border-bottom: 2px solid var(--canvas-accent-glow);
|
| 1803 |
+
padding-bottom: 6px;
|
| 1804 |
+
}
|
| 1805 |
+
.canvas-page-with-sidebar .poster-panel textarea {
|
| 1806 |
+
flex: 1;
|
| 1807 |
+
min-height: 120px;
|
| 1808 |
+
background: transparent;
|
| 1809 |
+
border: none;
|
| 1810 |
+
outline: none;
|
| 1811 |
+
resize: none;
|
| 1812 |
+
font-family: inherit;
|
| 1813 |
+
font-size: 13px;
|
| 1814 |
+
line-height: 1.5;
|
| 1815 |
+
color: var(--canvas-text);
|
| 1816 |
+
padding: 0;
|
| 1817 |
+
}
|
| 1818 |
+
@media (max-width: 768px) {
|
| 1819 |
+
.canvas-page-with-sidebar .poster-grid { grid-template-columns: 1fr; }
|
| 1820 |
+
}
|
| 1821 |
+
|
| 1822 |
+
/* ----- Rich editor block (click-to-edit + floating toolbar + LaTeX math) ----- */
|
| 1823 |
+
.canvas-page-with-sidebar .rich-block {
|
| 1824 |
+
position: relative;
|
| 1825 |
+
margin-top: 4px;
|
| 1826 |
+
}
|
| 1827 |
+
|
| 1828 |
+
.canvas-page-with-sidebar .rich-toolbar {
|
| 1829 |
+
position: absolute;
|
| 1830 |
+
bottom: calc(100% + 6px);
|
| 1831 |
+
left: 0;
|
| 1832 |
+
display: flex;
|
| 1833 |
+
flex-wrap: wrap;
|
| 1834 |
+
gap: 1px;
|
| 1835 |
+
padding: 4px;
|
| 1836 |
+
background: var(--canvas-surface);
|
| 1837 |
+
border: 1px solid var(--canvas-border-2);
|
| 1838 |
+
border-radius: 8px;
|
| 1839 |
+
box-shadow: var(--canvas-shadow-lg);
|
| 1840 |
+
z-index: 20;
|
| 1841 |
+
animation: canvas-view-in 140ms var(--canvas-ease);
|
| 1842 |
+
}
|
| 1843 |
+
.canvas-page-with-sidebar .rich-toolbar button {
|
| 1844 |
+
background: transparent;
|
| 1845 |
+
border: none;
|
| 1846 |
+
width: 30px;
|
| 1847 |
+
height: 28px;
|
| 1848 |
+
border-radius: 4px;
|
| 1849 |
+
display: grid;
|
| 1850 |
+
place-items: center;
|
| 1851 |
+
color: var(--canvas-text-2);
|
| 1852 |
+
font-family: inherit;
|
| 1853 |
+
font-size: 13px;
|
| 1854 |
+
cursor: pointer;
|
| 1855 |
+
transition: background .12s, color .12s;
|
| 1856 |
+
}
|
| 1857 |
+
.canvas-page-with-sidebar .rich-toolbar button:hover {
|
| 1858 |
+
background: var(--canvas-surface-2);
|
| 1859 |
+
color: var(--canvas-text);
|
| 1860 |
+
}
|
| 1861 |
+
|
| 1862 |
+
/* Rendered block (idle state — looks like real document text) */
|
| 1863 |
+
.canvas-page-with-sidebar .rich-rendered {
|
| 1864 |
+
cursor: text;
|
| 1865 |
+
padding: 4px 0;
|
| 1866 |
+
border-radius: 4px;
|
| 1867 |
+
transition: background .12s;
|
| 1868 |
+
outline: none;
|
| 1869 |
+
}
|
| 1870 |
+
.canvas-page-with-sidebar .rich-rendered:hover {
|
| 1871 |
+
background: rgba(255, 255, 255, 0.02);
|
| 1872 |
+
}
|
| 1873 |
+
.canvas-page-with-sidebar[data-canvas-theme="light"] .rich-rendered:hover {
|
| 1874 |
+
background: rgba(0, 0, 0, 0.02);
|
| 1875 |
+
}
|
| 1876 |
+
.canvas-page-with-sidebar .rich-rendered:focus-visible {
|
| 1877 |
+
background: rgba(0, 0, 0, 0.025);
|
| 1878 |
+
box-shadow: 0 0 0 2px var(--canvas-accent-glow);
|
| 1879 |
+
}
|
| 1880 |
+
.canvas-page-with-sidebar .rich-placeholder {
|
| 1881 |
+
color: var(--canvas-text-4);
|
| 1882 |
+
font-style: italic;
|
| 1883 |
+
}
|
| 1884 |
+
|
| 1885 |
+
/* Inside the rendered block: typography matching the page */
|
| 1886 |
+
.canvas-page-with-sidebar .rich-rendered > * { margin: 0; }
|
| 1887 |
+
.canvas-page-with-sidebar .rich-rendered > * + * { margin-top: 12px; }
|
| 1888 |
+
.canvas-page-with-sidebar .rich-rendered p {
|
| 1889 |
+
font-size: 16px;
|
| 1890 |
+
line-height: 1.65;
|
| 1891 |
+
color: var(--canvas-text);
|
| 1892 |
+
}
|
| 1893 |
+
.canvas-page-with-sidebar .rich-rendered.serif p {
|
| 1894 |
+
font-family: 'Georgia', 'Times New Roman', serif;
|
| 1895 |
+
font-size: 14.5px;
|
| 1896 |
+
line-height: 1.7;
|
| 1897 |
+
text-align: justify;
|
| 1898 |
+
}
|
| 1899 |
+
.canvas-page-with-sidebar .rich-rendered h1,
|
| 1900 |
+
.canvas-page-with-sidebar .rich-rendered h2,
|
| 1901 |
+
.canvas-page-with-sidebar .rich-rendered h3,
|
| 1902 |
+
.canvas-page-with-sidebar .rich-rendered h4 {
|
| 1903 |
+
font-weight: 700;
|
| 1904 |
+
letter-spacing: -0.01em;
|
| 1905 |
+
color: var(--canvas-text);
|
| 1906 |
+
margin-top: 16px;
|
| 1907 |
+
}
|
| 1908 |
+
.canvas-page-with-sidebar .rich-rendered h1 { font-size: 24px; }
|
| 1909 |
+
.canvas-page-with-sidebar .rich-rendered h2 { font-size: 19px; }
|
| 1910 |
+
.canvas-page-with-sidebar .rich-rendered h3 { font-size: 16px; }
|
| 1911 |
+
.canvas-page-with-sidebar .rich-rendered ul,
|
| 1912 |
+
.canvas-page-with-sidebar .rich-rendered ol {
|
| 1913 |
+
padding-left: 22px;
|
| 1914 |
+
font-size: 16px;
|
| 1915 |
+
line-height: 1.65;
|
| 1916 |
+
color: var(--canvas-text);
|
| 1917 |
+
}
|
| 1918 |
+
.canvas-page-with-sidebar .rich-rendered.serif ul,
|
| 1919 |
+
.canvas-page-with-sidebar .rich-rendered.serif ol {
|
| 1920 |
+
font-family: 'Georgia', serif;
|
| 1921 |
+
font-size: 14.5px;
|
| 1922 |
+
}
|
| 1923 |
+
.canvas-page-with-sidebar .rich-rendered li { margin-bottom: 4px; }
|
| 1924 |
+
.canvas-page-with-sidebar .rich-rendered blockquote {
|
| 1925 |
+
border-left: 3px solid var(--canvas-accent);
|
| 1926 |
+
padding: 4px 14px;
|
| 1927 |
+
color: var(--canvas-text-2);
|
| 1928 |
+
margin-left: 0;
|
| 1929 |
+
background: rgba(99, 102, 241, 0.04);
|
| 1930 |
+
border-radius: 0 4px 4px 0;
|
| 1931 |
+
}
|
| 1932 |
+
.canvas-page-with-sidebar .rich-rendered code {
|
| 1933 |
+
background: var(--canvas-surface-2);
|
| 1934 |
+
padding: 1px 5px;
|
| 1935 |
+
border-radius: 3px;
|
| 1936 |
+
font-family: var(--canvas-mono);
|
| 1937 |
+
font-size: 13px;
|
| 1938 |
+
}
|
| 1939 |
+
.canvas-page-with-sidebar .rich-rendered pre {
|
| 1940 |
+
background: var(--canvas-bg-2);
|
| 1941 |
+
border: 1px solid var(--canvas-border);
|
| 1942 |
+
border-radius: 6px;
|
| 1943 |
+
padding: 12px;
|
| 1944 |
+
overflow-x: auto;
|
| 1945 |
+
font-family: var(--canvas-mono);
|
| 1946 |
+
font-size: 13px;
|
| 1947 |
+
}
|
| 1948 |
+
.canvas-page-with-sidebar .rich-rendered pre code { background: none; padding: 0; }
|
| 1949 |
+
.canvas-page-with-sidebar .rich-rendered a {
|
| 1950 |
+
color: var(--canvas-accent);
|
| 1951 |
+
text-decoration: underline;
|
| 1952 |
+
text-underline-offset: 2px;
|
| 1953 |
+
}
|
| 1954 |
+
.canvas-page-with-sidebar .rich-rendered strong { color: var(--canvas-text); font-weight: 700; }
|
| 1955 |
+
.canvas-page-with-sidebar .rich-rendered em { color: var(--canvas-text); }
|
| 1956 |
+
.canvas-page-with-sidebar .rich-rendered img {
|
| 1957 |
+
max-width: 100%;
|
| 1958 |
+
height: auto;
|
| 1959 |
+
border-radius: 4px;
|
| 1960 |
+
margin: 8px 0;
|
| 1961 |
+
display: block;
|
| 1962 |
+
}
|
| 1963 |
+
|
| 1964 |
+
/* KaTeX math overrides — match document text size */
|
| 1965 |
+
.canvas-page-with-sidebar .rich-rendered .katex {
|
| 1966 |
+
font-size: 1.05em;
|
| 1967 |
+
color: var(--canvas-text);
|
| 1968 |
+
}
|
| 1969 |
+
.canvas-page-with-sidebar .rich-rendered .katex-display {
|
| 1970 |
+
margin: 16px 0;
|
| 1971 |
+
padding: 8px 12px;
|
| 1972 |
+
background: var(--canvas-bg-2);
|
| 1973 |
+
border-radius: 6px;
|
| 1974 |
+
overflow-x: auto;
|
| 1975 |
+
}
|
| 1976 |
+
.canvas-page-with-sidebar[data-canvas-theme="light"] .rich-rendered .katex-display {
|
| 1977 |
+
background: rgba(0, 0, 0, 0.025);
|
| 1978 |
+
}
|
| 1979 |
+
|
| 1980 |
/* ----- Notion-style single-surface deliverable editor ----- */
|
| 1981 |
.canvas-page-with-sidebar .notion-deliverable-grid {
|
| 1982 |
display: grid;
|