refactor: remove MUI, unify publisher pipeline, add shared component registry
Browse filesPhase 1 - Publisher pipeline:
- Extract ~370 lines of inline CSS from html-renderer.ts into _publisher.css
- Create shared/component-defs.ts as single source of truth for component definitions
- Replace 6 regex blocks in postProcess() with linkedom DOM manipulation
- Add GET /api/preview/:docName for testing renders without publishing
- Add 12 snapshot tests for each postProcess transformation (46 tests total)
Phase 2 - Remove MUI:
- Replace all MUI components with native HTML elements + custom CSS
- Create _ui.css with editor chrome styles using --ed-* CSS tokens
- Create Tooltip.tsx using Floating UI (lightweight replacement)
- Replace @mui /icons-material with lucide-react
- Delete theme.ts, remove @mui /* and @emotion /* dependencies
Phase 3 - Documentation:
- Add docs/ARCHITECTURE.md covering CSS layers, HF Spaces constraints,
shared registry, publisher pipeline, and test coverage
Made-with: Cursor
- Dockerfile +1 -0
- backend/package-lock.json +175 -0
- backend/package.json +1 -0
- backend/src/publisher/extensions.ts +6 -85
- backend/src/publisher/html-renderer.ts +119 -503
- backend/src/publisher/index.ts +53 -3
- backend/src/server.ts +37 -1
- backend/src/shared/component-defs.ts +94 -0
- backend/tests/__snapshots__/html-renderer-snapshot.test.ts.snap +475 -0
- backend/tests/html-renderer-snapshot.test.ts +209 -0
- backend/tests/publisher.test.ts +1 -0
- backend/tests/security.test.ts +1 -0
- docs/ARCHITECTURE.md +154 -0
- frontend/package-lock.json +28 -743
- frontend/package.json +1 -4
- frontend/src/App.tsx +107 -194
- frontend/src/components/ChatPanel.tsx +73 -182
- frontend/src/components/CommentDialog.tsx +41 -44
- frontend/src/components/CommentsSidebar.tsx +56 -84
- frontend/src/components/Tooltip.tsx +55 -0
- frontend/src/editor/BubbleToolbar.tsx +31 -49
- frontend/src/editor/FloatingActions.tsx +72 -87
- frontend/src/editor/components/registry.ts +77 -126
- frontend/src/editor/frontmatter/FrontmatterHero.tsx +56 -86
- frontend/src/editor/frontmatter/HueSlider.tsx +26 -35
- frontend/src/editor/frontmatter/SettingsDrawer.tsx +143 -178
- frontend/src/main.tsx +5 -5
- frontend/src/styles/_base.css +1 -9
- frontend/src/styles/_publisher.css +397 -0
- frontend/src/styles/_reset.css +5 -13
- frontend/src/styles/_ui.css +554 -0
- frontend/src/theme.ts +0 -47
- frontend/vite.config.ts +6 -0
|
@@ -5,6 +5,7 @@ WORKDIR /app/frontend
|
|
| 5 |
COPY frontend/package.json frontend/package-lock.json* frontend/.npmrc* ./
|
| 6 |
RUN npm install
|
| 7 |
COPY frontend/ ./
|
|
|
|
| 8 |
RUN npm run build
|
| 9 |
|
| 10 |
# --- Stage 2: Build backend ---
|
|
|
|
| 5 |
COPY frontend/package.json frontend/package-lock.json* frontend/.npmrc* ./
|
| 6 |
RUN npm install
|
| 7 |
COPY frontend/ ./
|
| 8 |
+
COPY backend/src/shared/ ../backend/src/shared/
|
| 9 |
RUN npm run build
|
| 10 |
|
| 11 |
# --- Stage 2: Build backend ---
|
|
@@ -34,6 +34,7 @@
|
|
| 34 |
"ai": "^6.0.158",
|
| 35 |
"dotenv": "^17.4.1",
|
| 36 |
"express": "^4.21.0",
|
|
|
|
| 37 |
"lowlight": "^3.3.0",
|
| 38 |
"multer": "^2.1.1",
|
| 39 |
"playwright": "^1.59.1",
|
|
@@ -2014,6 +2015,12 @@
|
|
| 2014 |
"npm": "1.2.8000 || >= 1.4.16"
|
| 2015 |
}
|
| 2016 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2017 |
"node_modules/buffer": {
|
| 2018 |
"version": "5.7.1",
|
| 2019 |
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
|
@@ -2195,6 +2202,40 @@
|
|
| 2195 |
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
| 2196 |
"license": "MIT"
|
| 2197 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2198 |
"node_modules/debug": {
|
| 2199 |
"version": "2.6.9",
|
| 2200 |
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
|
@@ -2255,6 +2296,73 @@
|
|
| 2255 |
"url": "https://github.com/sponsors/wooorm"
|
| 2256 |
}
|
| 2257 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2258 |
"node_modules/dotenv": {
|
| 2259 |
"version": "17.4.1",
|
| 2260 |
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz",
|
|
@@ -2697,6 +2805,31 @@
|
|
| 2697 |
"node": ">=12.0.0"
|
| 2698 |
}
|
| 2699 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2700 |
"node_modules/http-errors": {
|
| 2701 |
"version": "2.0.1",
|
| 2702 |
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
|
@@ -3098,6 +3231,30 @@
|
|
| 3098 |
"url": "https://opencollective.com/parcel"
|
| 3099 |
}
|
| 3100 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3101 |
"node_modules/linkify-it": {
|
| 3102 |
"version": "5.0.0",
|
| 3103 |
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
|
@@ -3322,6 +3479,18 @@
|
|
| 3322 |
}
|
| 3323 |
}
|
| 3324 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3325 |
"node_modules/object-inspect": {
|
| 3326 |
"version": "1.13.4",
|
| 3327 |
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
|
@@ -4174,6 +4343,12 @@
|
|
| 4174 |
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
| 4175 |
"license": "MIT"
|
| 4176 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4177 |
"node_modules/undici-types": {
|
| 4178 |
"version": "6.21.0",
|
| 4179 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
|
|
|
| 34 |
"ai": "^6.0.158",
|
| 35 |
"dotenv": "^17.4.1",
|
| 36 |
"express": "^4.21.0",
|
| 37 |
+
"linkedom": "^0.18.12",
|
| 38 |
"lowlight": "^3.3.0",
|
| 39 |
"multer": "^2.1.1",
|
| 40 |
"playwright": "^1.59.1",
|
|
|
|
| 2015 |
"npm": "1.2.8000 || >= 1.4.16"
|
| 2016 |
}
|
| 2017 |
},
|
| 2018 |
+
"node_modules/boolbase": {
|
| 2019 |
+
"version": "1.0.0",
|
| 2020 |
+
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
| 2021 |
+
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
| 2022 |
+
"license": "ISC"
|
| 2023 |
+
},
|
| 2024 |
"node_modules/buffer": {
|
| 2025 |
"version": "5.7.1",
|
| 2026 |
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
|
|
|
| 2202 |
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
| 2203 |
"license": "MIT"
|
| 2204 |
},
|
| 2205 |
+
"node_modules/css-select": {
|
| 2206 |
+
"version": "5.2.2",
|
| 2207 |
+
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
| 2208 |
+
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
| 2209 |
+
"license": "BSD-2-Clause",
|
| 2210 |
+
"dependencies": {
|
| 2211 |
+
"boolbase": "^1.0.0",
|
| 2212 |
+
"css-what": "^6.1.0",
|
| 2213 |
+
"domhandler": "^5.0.2",
|
| 2214 |
+
"domutils": "^3.0.1",
|
| 2215 |
+
"nth-check": "^2.0.1"
|
| 2216 |
+
},
|
| 2217 |
+
"funding": {
|
| 2218 |
+
"url": "https://github.com/sponsors/fb55"
|
| 2219 |
+
}
|
| 2220 |
+
},
|
| 2221 |
+
"node_modules/css-what": {
|
| 2222 |
+
"version": "6.2.2",
|
| 2223 |
+
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
| 2224 |
+
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
| 2225 |
+
"license": "BSD-2-Clause",
|
| 2226 |
+
"engines": {
|
| 2227 |
+
"node": ">= 6"
|
| 2228 |
+
},
|
| 2229 |
+
"funding": {
|
| 2230 |
+
"url": "https://github.com/sponsors/fb55"
|
| 2231 |
+
}
|
| 2232 |
+
},
|
| 2233 |
+
"node_modules/cssom": {
|
| 2234 |
+
"version": "0.5.0",
|
| 2235 |
+
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
|
| 2236 |
+
"integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
|
| 2237 |
+
"license": "MIT"
|
| 2238 |
+
},
|
| 2239 |
"node_modules/debug": {
|
| 2240 |
"version": "2.6.9",
|
| 2241 |
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
|
|
|
| 2296 |
"url": "https://github.com/sponsors/wooorm"
|
| 2297 |
}
|
| 2298 |
},
|
| 2299 |
+
"node_modules/dom-serializer": {
|
| 2300 |
+
"version": "2.0.0",
|
| 2301 |
+
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
| 2302 |
+
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
| 2303 |
+
"license": "MIT",
|
| 2304 |
+
"dependencies": {
|
| 2305 |
+
"domelementtype": "^2.3.0",
|
| 2306 |
+
"domhandler": "^5.0.2",
|
| 2307 |
+
"entities": "^4.2.0"
|
| 2308 |
+
},
|
| 2309 |
+
"funding": {
|
| 2310 |
+
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
| 2311 |
+
}
|
| 2312 |
+
},
|
| 2313 |
+
"node_modules/dom-serializer/node_modules/entities": {
|
| 2314 |
+
"version": "4.5.0",
|
| 2315 |
+
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
| 2316 |
+
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
| 2317 |
+
"license": "BSD-2-Clause",
|
| 2318 |
+
"engines": {
|
| 2319 |
+
"node": ">=0.12"
|
| 2320 |
+
},
|
| 2321 |
+
"funding": {
|
| 2322 |
+
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 2323 |
+
}
|
| 2324 |
+
},
|
| 2325 |
+
"node_modules/domelementtype": {
|
| 2326 |
+
"version": "2.3.0",
|
| 2327 |
+
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
| 2328 |
+
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
| 2329 |
+
"funding": [
|
| 2330 |
+
{
|
| 2331 |
+
"type": "github",
|
| 2332 |
+
"url": "https://github.com/sponsors/fb55"
|
| 2333 |
+
}
|
| 2334 |
+
],
|
| 2335 |
+
"license": "BSD-2-Clause"
|
| 2336 |
+
},
|
| 2337 |
+
"node_modules/domhandler": {
|
| 2338 |
+
"version": "5.0.3",
|
| 2339 |
+
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
| 2340 |
+
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
| 2341 |
+
"license": "BSD-2-Clause",
|
| 2342 |
+
"dependencies": {
|
| 2343 |
+
"domelementtype": "^2.3.0"
|
| 2344 |
+
},
|
| 2345 |
+
"engines": {
|
| 2346 |
+
"node": ">= 4"
|
| 2347 |
+
},
|
| 2348 |
+
"funding": {
|
| 2349 |
+
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
| 2350 |
+
}
|
| 2351 |
+
},
|
| 2352 |
+
"node_modules/domutils": {
|
| 2353 |
+
"version": "3.2.2",
|
| 2354 |
+
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
| 2355 |
+
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
| 2356 |
+
"license": "BSD-2-Clause",
|
| 2357 |
+
"dependencies": {
|
| 2358 |
+
"dom-serializer": "^2.0.0",
|
| 2359 |
+
"domelementtype": "^2.3.0",
|
| 2360 |
+
"domhandler": "^5.0.3"
|
| 2361 |
+
},
|
| 2362 |
+
"funding": {
|
| 2363 |
+
"url": "https://github.com/fb55/domutils?sponsor=1"
|
| 2364 |
+
}
|
| 2365 |
+
},
|
| 2366 |
"node_modules/dotenv": {
|
| 2367 |
"version": "17.4.1",
|
| 2368 |
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz",
|
|
|
|
| 2805 |
"node": ">=12.0.0"
|
| 2806 |
}
|
| 2807 |
},
|
| 2808 |
+
"node_modules/html-escaper": {
|
| 2809 |
+
"version": "3.0.3",
|
| 2810 |
+
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
|
| 2811 |
+
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
|
| 2812 |
+
"license": "MIT"
|
| 2813 |
+
},
|
| 2814 |
+
"node_modules/htmlparser2": {
|
| 2815 |
+
"version": "10.1.0",
|
| 2816 |
+
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
|
| 2817 |
+
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
|
| 2818 |
+
"funding": [
|
| 2819 |
+
"https://github.com/fb55/htmlparser2?sponsor=1",
|
| 2820 |
+
{
|
| 2821 |
+
"type": "github",
|
| 2822 |
+
"url": "https://github.com/sponsors/fb55"
|
| 2823 |
+
}
|
| 2824 |
+
],
|
| 2825 |
+
"license": "MIT",
|
| 2826 |
+
"dependencies": {
|
| 2827 |
+
"domelementtype": "^2.3.0",
|
| 2828 |
+
"domhandler": "^5.0.3",
|
| 2829 |
+
"domutils": "^3.2.2",
|
| 2830 |
+
"entities": "^7.0.1"
|
| 2831 |
+
}
|
| 2832 |
+
},
|
| 2833 |
"node_modules/http-errors": {
|
| 2834 |
"version": "2.0.1",
|
| 2835 |
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
|
|
|
| 3231 |
"url": "https://opencollective.com/parcel"
|
| 3232 |
}
|
| 3233 |
},
|
| 3234 |
+
"node_modules/linkedom": {
|
| 3235 |
+
"version": "0.18.12",
|
| 3236 |
+
"resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.12.tgz",
|
| 3237 |
+
"integrity": "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==",
|
| 3238 |
+
"license": "ISC",
|
| 3239 |
+
"dependencies": {
|
| 3240 |
+
"css-select": "^5.1.0",
|
| 3241 |
+
"cssom": "^0.5.0",
|
| 3242 |
+
"html-escaper": "^3.0.3",
|
| 3243 |
+
"htmlparser2": "^10.0.0",
|
| 3244 |
+
"uhyphen": "^0.2.0"
|
| 3245 |
+
},
|
| 3246 |
+
"engines": {
|
| 3247 |
+
"node": ">=16"
|
| 3248 |
+
},
|
| 3249 |
+
"peerDependencies": {
|
| 3250 |
+
"canvas": ">= 2"
|
| 3251 |
+
},
|
| 3252 |
+
"peerDependenciesMeta": {
|
| 3253 |
+
"canvas": {
|
| 3254 |
+
"optional": true
|
| 3255 |
+
}
|
| 3256 |
+
}
|
| 3257 |
+
},
|
| 3258 |
"node_modules/linkify-it": {
|
| 3259 |
"version": "5.0.0",
|
| 3260 |
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
|
|
|
| 3479 |
}
|
| 3480 |
}
|
| 3481 |
},
|
| 3482 |
+
"node_modules/nth-check": {
|
| 3483 |
+
"version": "2.1.1",
|
| 3484 |
+
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
| 3485 |
+
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
| 3486 |
+
"license": "BSD-2-Clause",
|
| 3487 |
+
"dependencies": {
|
| 3488 |
+
"boolbase": "^1.0.0"
|
| 3489 |
+
},
|
| 3490 |
+
"funding": {
|
| 3491 |
+
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
| 3492 |
+
}
|
| 3493 |
+
},
|
| 3494 |
"node_modules/object-inspect": {
|
| 3495 |
"version": "1.13.4",
|
| 3496 |
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
|
|
|
| 4343 |
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
| 4344 |
"license": "MIT"
|
| 4345 |
},
|
| 4346 |
+
"node_modules/uhyphen": {
|
| 4347 |
+
"version": "0.2.0",
|
| 4348 |
+
"resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",
|
| 4349 |
+
"integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",
|
| 4350 |
+
"license": "ISC"
|
| 4351 |
+
},
|
| 4352 |
"node_modules/undici-types": {
|
| 4353 |
"version": "6.21.0",
|
| 4354 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
|
@@ -37,6 +37,7 @@
|
|
| 37 |
"ai": "^6.0.158",
|
| 38 |
"dotenv": "^17.4.1",
|
| 39 |
"express": "^4.21.0",
|
|
|
|
| 40 |
"lowlight": "^3.3.0",
|
| 41 |
"multer": "^2.1.1",
|
| 42 |
"playwright": "^1.59.1",
|
|
|
|
| 37 |
"ai": "^6.0.158",
|
| 38 |
"dotenv": "^17.4.1",
|
| 39 |
"express": "^4.21.0",
|
| 40 |
+
"linkedom": "^0.18.12",
|
| 41 |
"lowlight": "^3.3.0",
|
| 42 |
"multer": "^2.1.1",
|
| 43 |
"playwright": "^1.59.1",
|
|
@@ -8,6 +8,7 @@
|
|
| 8 |
*/
|
| 9 |
|
| 10 |
import { Node, mergeAttributes, type Extensions } from "@tiptap/core";
|
|
|
|
| 11 |
import StarterKit from "@tiptap/starter-kit";
|
| 12 |
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
| 13 |
import Mathematics from "@tiptap/extension-mathematics";
|
|
@@ -173,89 +174,9 @@ const StackServer = Node.create({
|
|
| 173 |
},
|
| 174 |
});
|
| 175 |
|
| 176 |
-
// ---- Generic wrapper/atomic components from the registry ----
|
| 177 |
|
| 178 |
-
|
| 179 |
-
name: string;
|
| 180 |
-
kind: "wrapper" | "atomic";
|
| 181 |
-
fields: { name: string; type: string; default?: unknown }[];
|
| 182 |
-
content?: string;
|
| 183 |
-
}
|
| 184 |
-
|
| 185 |
-
const COMPONENT_DEFS: ComponentDefLite[] = [
|
| 186 |
-
{
|
| 187 |
-
name: "accordion",
|
| 188 |
-
kind: "wrapper",
|
| 189 |
-
fields: [
|
| 190 |
-
{ name: "title", type: "string", default: "Details" },
|
| 191 |
-
{ name: "open", type: "boolean", default: false },
|
| 192 |
-
],
|
| 193 |
-
},
|
| 194 |
-
{
|
| 195 |
-
name: "note",
|
| 196 |
-
kind: "wrapper",
|
| 197 |
-
fields: [
|
| 198 |
-
{ name: "title", type: "string", default: "" },
|
| 199 |
-
{ name: "emoji", type: "string", default: "" },
|
| 200 |
-
{ name: "variant", type: "select", default: "neutral" },
|
| 201 |
-
],
|
| 202 |
-
},
|
| 203 |
-
{
|
| 204 |
-
name: "quoteBlock",
|
| 205 |
-
kind: "wrapper",
|
| 206 |
-
fields: [
|
| 207 |
-
{ name: "author", type: "string", default: "" },
|
| 208 |
-
{ name: "source", type: "string", default: "" },
|
| 209 |
-
],
|
| 210 |
-
},
|
| 211 |
-
{ name: "wide", kind: "wrapper", fields: [] },
|
| 212 |
-
{ name: "fullWidth", kind: "wrapper", fields: [] },
|
| 213 |
-
{ name: "sidenote", kind: "wrapper", fields: [] },
|
| 214 |
-
{
|
| 215 |
-
name: "reference",
|
| 216 |
-
kind: "wrapper",
|
| 217 |
-
fields: [
|
| 218 |
-
{ name: "id", type: "string", default: "" },
|
| 219 |
-
{ name: "caption", type: "string", default: "" },
|
| 220 |
-
],
|
| 221 |
-
},
|
| 222 |
-
{
|
| 223 |
-
name: "htmlEmbed",
|
| 224 |
-
kind: "atomic",
|
| 225 |
-
fields: [
|
| 226 |
-
{ name: "src", type: "string", default: "" },
|
| 227 |
-
{ name: "title", type: "string", default: "" },
|
| 228 |
-
{ name: "desc", type: "string", default: "" },
|
| 229 |
-
{ name: "wide", type: "boolean", default: false },
|
| 230 |
-
{ name: "downloadable", type: "boolean", default: false },
|
| 231 |
-
],
|
| 232 |
-
},
|
| 233 |
-
{
|
| 234 |
-
name: "hfUser",
|
| 235 |
-
kind: "atomic",
|
| 236 |
-
fields: [
|
| 237 |
-
{ name: "username", type: "string", default: "" },
|
| 238 |
-
{ name: "name", type: "string", default: "" },
|
| 239 |
-
{ name: "url", type: "string", default: "" },
|
| 240 |
-
],
|
| 241 |
-
},
|
| 242 |
-
{
|
| 243 |
-
name: "rawHtml",
|
| 244 |
-
kind: "atomic",
|
| 245 |
-
fields: [
|
| 246 |
-
{ name: "html", type: "string", default: "" },
|
| 247 |
-
],
|
| 248 |
-
},
|
| 249 |
-
{
|
| 250 |
-
name: "mermaid",
|
| 251 |
-
kind: "atomic",
|
| 252 |
-
fields: [
|
| 253 |
-
{ name: "code", type: "string", default: "" },
|
| 254 |
-
],
|
| 255 |
-
},
|
| 256 |
-
];
|
| 257 |
-
|
| 258 |
-
function makeServerWrapperExt(def: ComponentDefLite) {
|
| 259 |
return Node.create({
|
| 260 |
name: def.name,
|
| 261 |
group: "block",
|
|
@@ -280,7 +201,7 @@ function makeServerWrapperExt(def: ComponentDefLite) {
|
|
| 280 |
});
|
| 281 |
}
|
| 282 |
|
| 283 |
-
function makeServerAtomicExt(def:
|
| 284 |
return Node.create({
|
| 285 |
name: def.name,
|
| 286 |
group: "block",
|
|
@@ -303,8 +224,8 @@ function makeServerAtomicExt(def: ComponentDefLite) {
|
|
| 303 |
}
|
| 304 |
|
| 305 |
export function getServerExtensions(): Extensions {
|
| 306 |
-
const wrappers =
|
| 307 |
-
const atomics =
|
| 308 |
|
| 309 |
return [
|
| 310 |
StarterKit.configure({
|
|
|
|
| 8 |
*/
|
| 9 |
|
| 10 |
import { Node, mergeAttributes, type Extensions } from "@tiptap/core";
|
| 11 |
+
import { SHARED_COMPONENT_DEFS, type SharedComponentDef } from "../shared/component-defs.js";
|
| 12 |
import StarterKit from "@tiptap/starter-kit";
|
| 13 |
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
| 14 |
import Mathematics from "@tiptap/extension-mathematics";
|
|
|
|
| 174 |
},
|
| 175 |
});
|
| 176 |
|
| 177 |
+
// ---- Generic wrapper/atomic components from the shared registry ----
|
| 178 |
|
| 179 |
+
function makeServerWrapperExt(def: SharedComponentDef) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
return Node.create({
|
| 181 |
name: def.name,
|
| 182 |
group: "block",
|
|
|
|
| 201 |
});
|
| 202 |
}
|
| 203 |
|
| 204 |
+
function makeServerAtomicExt(def: SharedComponentDef) {
|
| 205 |
return Node.create({
|
| 206 |
name: def.name,
|
| 207 |
group: "block",
|
|
|
|
| 224 |
}
|
| 225 |
|
| 226 |
export function getServerExtensions(): Extensions {
|
| 227 |
+
const wrappers = SHARED_COMPONENT_DEFS.filter((d) => d.kind === "wrapper").map(makeServerWrapperExt);
|
| 228 |
+
const atomics = SHARED_COMPONENT_DEFS.filter((d) => d.kind === "atomic").map(makeServerAtomicExt);
|
| 229 |
|
| 230 |
return [
|
| 231 |
StarterKit.configure({
|
|
@@ -6,6 +6,7 @@
|
|
| 6 |
*/
|
| 7 |
|
| 8 |
import { generateHTML } from "@tiptap/html";
|
|
|
|
| 9 |
import { getServerExtensions } from "./extensions.js";
|
| 10 |
import type { PublishCSS } from "./index.js";
|
| 11 |
|
|
@@ -105,411 +106,16 @@ export function renderArticleHTML(
|
|
| 105 |
</script>
|
| 106 |
|
| 107 |
<style>
|
| 108 |
-
/* Template foundation */
|
| 109 |
${css.variables}
|
| 110 |
${css.reset}
|
| 111 |
-
|
| 112 |
-
/* Published page global reset (not needed in editor where MUI handles body) */
|
| 113 |
-
body { font-family: var(--default-font-family); color: var(--text-color); }
|
| 114 |
-
html { font-size: 16px; line-height: 1.6; background-color: var(--page-bg); overflow-x: hidden; scroll-behavior: smooth; scroll-padding-top: 80px; }
|
| 115 |
-
|
| 116 |
${css.editorTokens}
|
| 117 |
${css.base}
|
| 118 |
${css.layout}
|
| 119 |
${css.components}
|
| 120 |
${css.article}
|
| 121 |
${css.print}
|
|
|
|
| 122 |
|
| 123 |
-
/* Publisher-only overrides */
|
| 124 |
-
|
| 125 |
-
/* Theme toggle icon wrapper (from ThemeToggle.astro) */
|
| 126 |
-
#theme-toggle .icon-wrapper {
|
| 127 |
-
display: grid;
|
| 128 |
-
place-items: center;
|
| 129 |
-
width: 20px;
|
| 130 |
-
height: 20px;
|
| 131 |
-
}
|
| 132 |
-
#theme-toggle .icon-wrapper .icon {
|
| 133 |
-
grid-area: 1 / 1;
|
| 134 |
-
filter: none !important;
|
| 135 |
-
}
|
| 136 |
-
#theme-toggle .icon-wrapper.animated .icon {
|
| 137 |
-
transition: opacity 0.35s ease;
|
| 138 |
-
}
|
| 139 |
-
#theme-toggle .icon-wrapper.spin-cw { animation: spin-cw 0.5s cubic-bezier(0.4,0,0.2,1); }
|
| 140 |
-
#theme-toggle .icon-wrapper.spin-ccw { animation: spin-ccw 0.5s cubic-bezier(0.4,0,0.2,1); }
|
| 141 |
-
@keyframes spin-cw { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
| 142 |
-
@keyframes spin-ccw { from { transform: rotate(0deg); } to { transform: rotate(-360deg); } }
|
| 143 |
-
|
| 144 |
-
/* Note component - aligned with template Note.astro */
|
| 145 |
-
div[data-component="note"] {
|
| 146 |
-
background: var(--surface-bg);
|
| 147 |
-
border-left: 2px solid var(--border-color);
|
| 148 |
-
border-top-right-radius: 8px;
|
| 149 |
-
border-bottom-right-radius: 8px;
|
| 150 |
-
padding: 20px 18px;
|
| 151 |
-
margin: var(--block-spacing-y, 1em) 0;
|
| 152 |
-
}
|
| 153 |
-
div[data-component="note"][variant="info"] {
|
| 154 |
-
border-left-color: #f39c12;
|
| 155 |
-
background: color-mix(in oklab, #f39c12 10%, var(--surface-bg));
|
| 156 |
-
}
|
| 157 |
-
div[data-component="note"][variant="success"] {
|
| 158 |
-
border-left-color: #2ecc71;
|
| 159 |
-
background: color-mix(in oklab, #2ecc71 8%, var(--surface-bg));
|
| 160 |
-
}
|
| 161 |
-
div[data-component="note"][variant="danger"] {
|
| 162 |
-
border-left-color: #e74c3c;
|
| 163 |
-
background: color-mix(in oklab, #e74c3c 8%, var(--surface-bg));
|
| 164 |
-
}
|
| 165 |
-
div[data-component="note"] .note__title {
|
| 166 |
-
font-size: 16px;
|
| 167 |
-
font-weight: 600;
|
| 168 |
-
color: var(--text-color);
|
| 169 |
-
margin-bottom: 6px;
|
| 170 |
-
}
|
| 171 |
-
div[data-component="note"] > *:first-child { margin-top: 0; }
|
| 172 |
-
div[data-component="note"] > *:last-child { margin-bottom: 0; }
|
| 173 |
-
|
| 174 |
-
/* Quote component - aligned with template Quote.astro */
|
| 175 |
-
div[data-component="quoteBlock"] {
|
| 176 |
-
position: relative;
|
| 177 |
-
margin: 32px 0;
|
| 178 |
-
max-width: 600px;
|
| 179 |
-
padding: 0;
|
| 180 |
-
border: none;
|
| 181 |
-
background: none;
|
| 182 |
-
}
|
| 183 |
-
div[data-component="quoteBlock"]::before {
|
| 184 |
-
content: '\\201C';
|
| 185 |
-
position: absolute;
|
| 186 |
-
top: -24px;
|
| 187 |
-
left: -30px;
|
| 188 |
-
font-size: 8rem;
|
| 189 |
-
font-weight: 400;
|
| 190 |
-
color: var(--text-color);
|
| 191 |
-
opacity: 0.05;
|
| 192 |
-
z-index: -1;
|
| 193 |
-
line-height: 1;
|
| 194 |
-
pointer-events: none;
|
| 195 |
-
}
|
| 196 |
-
div[data-component="quoteBlock"] .quote-text {
|
| 197 |
-
font-size: 1.5rem;
|
| 198 |
-
line-height: 1.4;
|
| 199 |
-
font-weight: 400;
|
| 200 |
-
letter-spacing: -0.01em;
|
| 201 |
-
color: var(--text-color);
|
| 202 |
-
}
|
| 203 |
-
div[data-component="quoteBlock"] .quote-footer {
|
| 204 |
-
font-size: 0.875rem;
|
| 205 |
-
color: var(--muted-color);
|
| 206 |
-
margin-top: 12px;
|
| 207 |
-
}
|
| 208 |
-
div[data-component="quoteBlock"] .quote-author::before {
|
| 209 |
-
content: '\\2014\\00A0';
|
| 210 |
-
font-style: normal;
|
| 211 |
-
}
|
| 212 |
-
div[data-component="quoteBlock"] .quote-author {
|
| 213 |
-
font-weight: 500;
|
| 214 |
-
font-style: italic;
|
| 215 |
-
color: var(--text-color);
|
| 216 |
-
opacity: 0.85;
|
| 217 |
-
}
|
| 218 |
-
div[data-component="quoteBlock"] .quote-source {
|
| 219 |
-
font-weight: 500;
|
| 220 |
-
font-style: italic;
|
| 221 |
-
color: var(--text-color);
|
| 222 |
-
opacity: 0.85;
|
| 223 |
-
}
|
| 224 |
-
div[data-component="quoteBlock"] > *:first-child { margin-top: 0; }
|
| 225 |
-
div[data-component="quoteBlock"] > *:last-child { margin-bottom: 0; }
|
| 226 |
-
|
| 227 |
-
/* Sidenote component - aligned with template Sidenote.astro */
|
| 228 |
-
div[data-component="sidenote"] {
|
| 229 |
-
font-size: 0.9rem;
|
| 230 |
-
color: var(--muted-color);
|
| 231 |
-
padding: 0 30px;
|
| 232 |
-
margin: 1em 0;
|
| 233 |
-
}
|
| 234 |
-
div[data-component="sidenote"] > *:first-child { margin-top: 0; }
|
| 235 |
-
div[data-component="sidenote"] > *:last-child { margin-bottom: 0; }
|
| 236 |
-
|
| 237 |
-
/* Reference wrapper - aligned with template Reference.astro */
|
| 238 |
-
div[data-component="reference"] {
|
| 239 |
-
margin: 0 0 var(--spacing-4, 1rem);
|
| 240 |
-
}
|
| 241 |
-
div[data-component="reference"] .reference__caption {
|
| 242 |
-
text-align: left;
|
| 243 |
-
font-size: 0.9rem;
|
| 244 |
-
color: var(--muted-color);
|
| 245 |
-
margin-top: 6px;
|
| 246 |
-
}
|
| 247 |
-
div[data-component="reference"] > *:first-child { margin-top: 0; }
|
| 248 |
-
div[data-component="reference"] > *:last-child { margin-bottom: 0; }
|
| 249 |
-
|
| 250 |
-
/* Stack / multi-column layout - aligned with template Stack.astro */
|
| 251 |
-
div[data-component="stack"] {
|
| 252 |
-
display: grid;
|
| 253 |
-
gap: 1rem;
|
| 254 |
-
margin: var(--block-spacing-y, 1.5em) 0;
|
| 255 |
-
width: 100%;
|
| 256 |
-
max-width: 100%;
|
| 257 |
-
box-sizing: border-box;
|
| 258 |
-
}
|
| 259 |
-
div[data-component="stack"][data-layout="2-column"] { grid-template-columns: repeat(2, 1fr); }
|
| 260 |
-
div[data-component="stack"][data-layout="3-column"] { grid-template-columns: repeat(3, 1fr); }
|
| 261 |
-
div[data-component="stack"][data-layout="4-column"] { grid-template-columns: repeat(4, 1fr); }
|
| 262 |
-
div[data-component="stack"][data-layout="auto"] { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
|
| 263 |
-
div[data-component="stack"]:not([data-layout]) { grid-template-columns: repeat(2, 1fr); }
|
| 264 |
-
div[data-component="stack"][data-gap="small"] { gap: 0.5rem; }
|
| 265 |
-
div[data-component="stack"][data-gap="large"] { gap: 2rem; }
|
| 266 |
-
div[data-type="stack-column"] { min-width: 0; overflow: hidden; word-wrap: break-word; overflow-wrap: break-word; }
|
| 267 |
-
div[data-type="stack-column"] > *:first-child { margin-top: 0; }
|
| 268 |
-
div[data-type="stack-column"] > *:last-child { margin-bottom: 0; }
|
| 269 |
-
@media (max-width: 768px) {
|
| 270 |
-
div[data-component="stack"][data-layout="2-column"],
|
| 271 |
-
div[data-component="stack"][data-layout="3-column"],
|
| 272 |
-
div[data-component="stack"][data-layout="4-column"],
|
| 273 |
-
div[data-component="stack"][data-layout="auto"],
|
| 274 |
-
div[data-component="stack"]:not([data-layout]) { grid-template-columns: 1fr !important; }
|
| 275 |
-
}
|
| 276 |
-
@media (min-width: 769px) and (max-width: 1100px) {
|
| 277 |
-
div[data-component="stack"][data-layout="3-column"],
|
| 278 |
-
div[data-component="stack"][data-layout="4-column"],
|
| 279 |
-
div[data-component="stack"][data-layout="auto"] { grid-template-columns: repeat(2, 1fr) !important; }
|
| 280 |
-
}
|
| 281 |
-
|
| 282 |
-
/* Wide / full-width helpers */
|
| 283 |
-
div[data-component="wide"] {
|
| 284 |
-
width: min(1100px, 100vw - 64px);
|
| 285 |
-
margin-left: 50%;
|
| 286 |
-
transform: translateX(-50%);
|
| 287 |
-
}
|
| 288 |
-
div[data-component="fullWidth"] {
|
| 289 |
-
width: 100vw;
|
| 290 |
-
margin-left: calc(50% - 50vw);
|
| 291 |
-
}
|
| 292 |
-
|
| 293 |
-
/* Accordion - aligned with template Accordion.astro */
|
| 294 |
-
details[data-component="accordion"] {
|
| 295 |
-
border: 1px solid var(--border-color);
|
| 296 |
-
border-radius: var(--table-border-radius, 8px);
|
| 297 |
-
background: var(--surface-bg);
|
| 298 |
-
padding: 0;
|
| 299 |
-
margin: 0 0 var(--spacing-4, 1em);
|
| 300 |
-
transition: box-shadow 180ms ease, border-color 180ms ease;
|
| 301 |
-
}
|
| 302 |
-
details[data-component="accordion"][open] {
|
| 303 |
-
border-color: color-mix(in oklab, var(--border-color), var(--primary-color) 20%);
|
| 304 |
-
}
|
| 305 |
-
details[data-component="accordion"] > summary {
|
| 306 |
-
display: flex;
|
| 307 |
-
align-items: center;
|
| 308 |
-
justify-content: space-between;
|
| 309 |
-
gap: 4px;
|
| 310 |
-
padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);
|
| 311 |
-
cursor: pointer;
|
| 312 |
-
font-weight: 600;
|
| 313 |
-
color: var(--text-color);
|
| 314 |
-
list-style: none;
|
| 315 |
-
user-select: none;
|
| 316 |
-
}
|
| 317 |
-
details[data-component="accordion"][open] > summary {
|
| 318 |
-
border-bottom: 1px solid var(--border-color);
|
| 319 |
-
}
|
| 320 |
-
details[data-component="accordion"] > summary::-webkit-details-marker { display: none; }
|
| 321 |
-
details[data-component="accordion"] > summary::marker { content: ""; }
|
| 322 |
-
details[data-component="accordion"] > summary::after {
|
| 323 |
-
content: '';
|
| 324 |
-
display: inline-block;
|
| 325 |
-
width: 0.5em;
|
| 326 |
-
height: 0.5em;
|
| 327 |
-
border-right: 2px solid currentColor;
|
| 328 |
-
border-bottom: 2px solid currentColor;
|
| 329 |
-
transform: rotate(-45deg);
|
| 330 |
-
transition: transform 220ms ease;
|
| 331 |
-
opacity: 0.6;
|
| 332 |
-
flex-shrink: 0;
|
| 333 |
-
}
|
| 334 |
-
details[data-component="accordion"][open] > summary::after {
|
| 335 |
-
transform: rotate(45deg);
|
| 336 |
-
}
|
| 337 |
-
details[data-component="accordion"] > .accordion-content {
|
| 338 |
-
padding: 8px;
|
| 339 |
-
}
|
| 340 |
-
details[data-component="accordion"] > .accordion-content > *:first-child { margin-top: 0; }
|
| 341 |
-
details[data-component="accordion"] > .accordion-content > *:last-child { margin-bottom: 0; }
|
| 342 |
-
|
| 343 |
-
/* Footer */
|
| 344 |
-
.footer {
|
| 345 |
-
contain: layout style;
|
| 346 |
-
font-size: 0.8em;
|
| 347 |
-
line-height: 1.7em;
|
| 348 |
-
margin-top: 60px;
|
| 349 |
-
margin-bottom: 0;
|
| 350 |
-
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
| 351 |
-
color: rgba(0, 0, 0, 0.5);
|
| 352 |
-
}
|
| 353 |
-
|
| 354 |
-
.footer-inner {
|
| 355 |
-
max-width: 1280px;
|
| 356 |
-
margin: 0 auto;
|
| 357 |
-
padding: 60px 16px 48px;
|
| 358 |
-
display: grid;
|
| 359 |
-
grid-template-columns: 220px minmax(0, 680px) 260px;
|
| 360 |
-
gap: 32px;
|
| 361 |
-
align-items: start;
|
| 362 |
-
}
|
| 363 |
-
|
| 364 |
-
.citation-block,
|
| 365 |
-
.references-block,
|
| 366 |
-
.reuse-block,
|
| 367 |
-
.doi-block {
|
| 368 |
-
display: contents;
|
| 369 |
-
}
|
| 370 |
-
|
| 371 |
-
.citation-block > .footer-heading,
|
| 372 |
-
.references-block > .footer-heading,
|
| 373 |
-
.reuse-block > .footer-heading,
|
| 374 |
-
.doi-block > .footer-heading {
|
| 375 |
-
grid-column: 1;
|
| 376 |
-
font-size: 15px;
|
| 377 |
-
font-weight: 600;
|
| 378 |
-
margin: 0;
|
| 379 |
-
text-align: right;
|
| 380 |
-
padding-right: 30px;
|
| 381 |
-
}
|
| 382 |
-
|
| 383 |
-
.citation-block > :not(.footer-heading),
|
| 384 |
-
.references-block > :not(.footer-heading),
|
| 385 |
-
.reuse-block > :not(.footer-heading),
|
| 386 |
-
.doi-block > :not(.footer-heading) {
|
| 387 |
-
grid-column: 2;
|
| 388 |
-
}
|
| 389 |
-
|
| 390 |
-
.citation-block .footer-heading { margin: 0 0 8px; }
|
| 391 |
-
|
| 392 |
-
.citation-block p,
|
| 393 |
-
.reuse-block p,
|
| 394 |
-
.doi-block p,
|
| 395 |
-
.footnotes ol,
|
| 396 |
-
.footnotes ol p,
|
| 397 |
-
.references { margin-top: 0; }
|
| 398 |
-
|
| 399 |
-
.footnote-ref a {
|
| 400 |
-
color: var(--primary-color, #958DF1);
|
| 401 |
-
text-decoration: none;
|
| 402 |
-
font-size: 0.8em;
|
| 403 |
-
}
|
| 404 |
-
.footnote-ref a:hover { text-decoration: underline; }
|
| 405 |
-
.footnotes ol { padding-left: 1.5em; }
|
| 406 |
-
.footnotes li { margin-bottom: 0.5em; font-size: 0.9rem; color: var(--muted-color, #888); }
|
| 407 |
-
.footnotes li p { margin: 0; }
|
| 408 |
-
.footnote-backref { text-decoration: none; margin-left: 4px; }
|
| 409 |
-
|
| 410 |
-
.citation {
|
| 411 |
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 412 |
-
font-size: 11px;
|
| 413 |
-
line-height: 15px;
|
| 414 |
-
border: 1px solid rgba(0, 0, 0, 0.1);
|
| 415 |
-
background: rgba(0, 0, 0, 0.02);
|
| 416 |
-
padding: 10px 18px;
|
| 417 |
-
border-radius: 3px;
|
| 418 |
-
color: rgba(150, 150, 150, 1);
|
| 419 |
-
overflow: hidden;
|
| 420 |
-
margin-top: -12px;
|
| 421 |
-
white-space: pre-wrap;
|
| 422 |
-
word-wrap: break-word;
|
| 423 |
-
}
|
| 424 |
-
|
| 425 |
-
.citation a { color: rgba(0, 0, 0, 0.6); text-decoration: underline; }
|
| 426 |
-
.citation.short { margin-top: -4px; }
|
| 427 |
-
|
| 428 |
-
.citation-inline {
|
| 429 |
-
color: var(--primary-color, #958df1);
|
| 430 |
-
text-decoration: none;
|
| 431 |
-
text-decoration-line: underline;
|
| 432 |
-
text-decoration-color: transparent;
|
| 433 |
-
text-underline-offset: 2px;
|
| 434 |
-
cursor: pointer;
|
| 435 |
-
transition: text-decoration-color 0.15s;
|
| 436 |
-
}
|
| 437 |
-
.citation-inline:hover {
|
| 438 |
-
text-decoration-color: currentColor;
|
| 439 |
-
}
|
| 440 |
-
|
| 441 |
-
.bibliography-content .csl-entry {
|
| 442 |
-
margin-bottom: 0.75em;
|
| 443 |
-
padding-left: 1.5em;
|
| 444 |
-
text-indent: -1.5em;
|
| 445 |
-
font-size: 0.9em;
|
| 446 |
-
line-height: 1.5;
|
| 447 |
-
}
|
| 448 |
-
.bibliography-content .csl-entry:target {
|
| 449 |
-
background: rgba(149, 141, 241, 0.1);
|
| 450 |
-
border-radius: 4px;
|
| 451 |
-
padding: 4px 4px 4px 1.5em;
|
| 452 |
-
}
|
| 453 |
-
|
| 454 |
-
.references-block .footer-heading { margin: 0; }
|
| 455 |
-
.references-block ol { padding: 0 0 0 15px; }
|
| 456 |
-
.references-block li { margin-bottom: 1em; }
|
| 457 |
-
.references-block a { color: var(--text-color); }
|
| 458 |
-
|
| 459 |
-
.footer a {
|
| 460 |
-
color: var(--primary-color);
|
| 461 |
-
border-bottom: 1px solid var(--link-underline);
|
| 462 |
-
text-decoration: none;
|
| 463 |
-
}
|
| 464 |
-
.footer a:hover {
|
| 465 |
-
color: var(--primary-color-hover);
|
| 466 |
-
border-bottom-color: var(--link-underline-hover);
|
| 467 |
-
}
|
| 468 |
-
|
| 469 |
-
.template-credit { display: contents; }
|
| 470 |
-
.template-credit p {
|
| 471 |
-
grid-column: 2;
|
| 472 |
-
margin: 24px 0 0 0;
|
| 473 |
-
font-size: 0.85em;
|
| 474 |
-
color: rgba(0, 0, 0, 0.5);
|
| 475 |
-
}
|
| 476 |
-
.template-credit a { color: rgba(0, 0, 0, 0.6); border-bottom: 1px solid rgba(0, 0, 0, 0.15); }
|
| 477 |
-
.template-credit a:hover { color: rgba(0, 0, 0, 0.8); border-bottom-color: rgba(0, 0, 0, 0.3); }
|
| 478 |
-
|
| 479 |
-
[data-theme="dark"] .footer { border-top-color: rgba(255, 255, 255, 0.15); color: rgba(200, 200, 200, 0.8); }
|
| 480 |
-
[data-theme="dark"] .citation { background: rgba(255, 255, 255, 0.04); border-color: rgba(255, 255, 255, 0.15); color: rgba(200, 200, 200, 1); }
|
| 481 |
-
[data-theme="dark"] .citation a { color: rgba(255, 255, 255, 0.75); }
|
| 482 |
-
[data-theme="dark"] .bibliography-content .csl-entry:target { background: rgba(149, 141, 241, 0.15); }
|
| 483 |
-
[data-theme="dark"] .footer a { color: var(--primary-color); }
|
| 484 |
-
[data-theme="dark"] .template-credit p { color: rgba(200, 200, 200, 0.6); }
|
| 485 |
-
[data-theme="dark"] .template-credit a { color: rgba(200, 200, 200, 0.7); border-bottom-color: rgba(255, 255, 255, 0.2); }
|
| 486 |
-
[data-theme="dark"] .template-credit a:hover { color: rgba(200, 200, 200, 0.9); border-bottom-color: rgba(255, 255, 255, 0.35); }
|
| 487 |
-
|
| 488 |
-
@media (max-width: 1100px) {
|
| 489 |
-
.footer-inner { grid-template-columns: 1fr; gap: 16px; display: block; padding: 40px 16px; }
|
| 490 |
-
.footer-inner > .footer-heading { grid-column: auto; margin-top: 16px; }
|
| 491 |
-
}
|
| 492 |
-
|
| 493 |
-
@media (min-width: 768px) {
|
| 494 |
-
.references-block ol { padding: 0 0 0 30px; margin-left: -30px; }
|
| 495 |
-
}
|
| 496 |
-
|
| 497 |
-
/* PDF download link */
|
| 498 |
-
a.pdf-link {
|
| 499 |
-
display: inline-flex;
|
| 500 |
-
align-items: center;
|
| 501 |
-
gap: 0.4em;
|
| 502 |
-
color: var(--accent-color, #4493f8);
|
| 503 |
-
text-decoration: none;
|
| 504 |
-
font-weight: 500;
|
| 505 |
-
}
|
| 506 |
-
a.pdf-link:hover { text-decoration: underline; }
|
| 507 |
-
a.pdf-link::before { content: "\\1F4C4"; }
|
| 508 |
-
|
| 509 |
-
/* Image lightbox dialog */
|
| 510 |
-
dialog.lightbox { border: none; background: transparent; padding: 0; max-width: 95vw; max-height: 95vh; }
|
| 511 |
-
dialog.lightbox::backdrop { background: rgba(0, 0, 0, 0.85); }
|
| 512 |
-
dialog.lightbox img { max-width: 95vw; max-height: 90vh; object-fit: contain; border-radius: 4px; }
|
| 513 |
</style>
|
| 514 |
</head>
|
| 515 |
<body>
|
|
@@ -944,120 +550,132 @@ function extractBibliographyHtml(json: Record<string, unknown>): string {
|
|
| 944 |
}
|
| 945 |
|
| 946 |
/**
|
| 947 |
-
* Post-process the raw generateHTML output:
|
| 948 |
-
* - Inject bibliography HTML (extracted before generateHTML)
|
| 949 |
* - Transform accordion divs into <details><summary>
|
| 950 |
-
* -
|
|
|
|
|
|
|
|
|
|
|
|
|
| 951 |
*/
|
| 952 |
function postProcess(html: string, biblioHtml: string, citationData?: CitationData): string {
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
// Accordion: div[data-component="accordion"]
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 968 |
|
| 969 |
-
// Inline citations:
|
| 970 |
-
// with proper numbered links pointing to bibliography entries.
|
| 971 |
const NUMERIC_STYLES = new Set(["ieee", "vancouver"]);
|
| 972 |
const isNumeric = citationData ? NUMERIC_STYLES.has(citationData.style) : false;
|
| 973 |
const citationKeyOrder: string[] = [];
|
| 974 |
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
(
|
| 978 |
-
const keyMatch = match.match(/(?:\skey="|data-key=")([^"]*)"/);
|
| 979 |
-
if (!keyMatch) return match;
|
| 980 |
-
const key = keyMatch[1];
|
| 981 |
|
| 982 |
-
|
| 983 |
-
|
| 984 |
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
const originalLabel = labelMatch ? labelMatch[1] : `[${key}]`;
|
| 988 |
|
| 989 |
-
|
| 990 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 991 |
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
// Wrap bibliography HTML with entry anchors
|
| 1004 |
-
let enrichedBiblio = biblioHtml;
|
| 1005 |
-
if (citationData && citationData.orderedKeys.length > 0) {
|
| 1006 |
-
enrichedBiblio = addBibliographyAnchors(biblioHtml, citationData.orderedKeys);
|
| 1007 |
-
}
|
| 1008 |
-
return `<div${cleanAttrs}><h2 class="bibliography-title">References</h2><div class="bibliography-content">${enrichedBiblio}</div></div>`;
|
| 1009 |
}
|
| 1010 |
-
|
| 1011 |
-
}
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
// Mermaid: div[data-component="mermaid"] → <pre class="mermaid">code</pre>
|
| 1015 |
-
result = result.replace(
|
| 1016 |
-
/<div[^>]*data-component="mermaid"[^>]*><\/div>/g,
|
| 1017 |
-
(match) => {
|
| 1018 |
-
const codeMatch = match.match(/data-code="([^"]*)"/);
|
| 1019 |
-
const code = codeMatch ? codeMatch[1].replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'") : "";
|
| 1020 |
-
return `<pre class="mermaid">${escapeHtml(code)}</pre>`;
|
| 1021 |
-
}
|
| 1022 |
-
);
|
| 1023 |
-
|
| 1024 |
-
// HtmlEmbed: div[data-component="htmlEmbed"] → iframe with srcdoc
|
| 1025 |
-
result = result.replace(
|
| 1026 |
-
/<div[^>]*data-component="htmlEmbed"[^>]*data-src="([^"]*)"[^>]*><\/div>/g,
|
| 1027 |
-
(_match, src) => {
|
| 1028 |
-
return `<div class="html-embed-container">
|
| 1029 |
-
<iframe srcdoc="" data-embed-src="${escapeHtml(src)}"
|
| 1030 |
-
style="width:100%;border:none;min-height:400px;"
|
| 1031 |
-
loading="lazy" sandbox="allow-scripts"></iframe>
|
| 1032 |
-
</div>`;
|
| 1033 |
-
}
|
| 1034 |
-
);
|
| 1035 |
-
|
| 1036 |
-
// Footnotes: collect all <span data-type="footnote">, number them,
|
| 1037 |
-
// replace inline markers with superscript links, and append a
|
| 1038 |
-
// footnotes section at the bottom.
|
| 1039 |
-
const footnotes: string[] = [];
|
| 1040 |
-
result = result.replace(
|
| 1041 |
-
/<span[^>]*data-type="footnote"[^>]*>[\s\S]*?<\/span>/g,
|
| 1042 |
-
(match) => {
|
| 1043 |
-
const contentMatch = match.match(/(?:data-content|title)="([^"]*)"/);
|
| 1044 |
-
const raw = contentMatch ? contentMatch[1] : "";
|
| 1045 |
-
const text = raw
|
| 1046 |
-
.replace(/&/g, "&").replace(/</g, "<")
|
| 1047 |
-
.replace(/>/g, ">").replace(/"/g, '"')
|
| 1048 |
-
.replace(/'/g, "'");
|
| 1049 |
-
footnotes.push(text);
|
| 1050 |
-
const idx = footnotes.length;
|
| 1051 |
-
return `<sup class="footnote-ref"><a href="#fn-${idx}" id="fnref-${idx}">[${idx}]</a></sup>`;
|
| 1052 |
}
|
| 1053 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1054 |
|
| 1055 |
-
|
| 1056 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1057 |
.map((text, i) => `<li id="fn-${i + 1}"><p>${escapeHtml(text)} <a href="#fnref-${i + 1}" class="footnote-backref" aria-label="Back to text">\u21A9</a></p></li>`)
|
| 1058 |
.join("\n");
|
| 1059 |
-
|
| 1060 |
-
result += footnotesSection;
|
| 1061 |
}
|
| 1062 |
|
| 1063 |
return result;
|
|
@@ -1066,18 +684,16 @@ function postProcess(html: string, biblioHtml: string, citationData?: CitationDa
|
|
| 1066 |
/**
|
| 1067 |
* Add id anchors to bibliography entries so inline citations can link to them.
|
| 1068 |
* citation-js outputs entries as <div class="csl-entry"> elements.
|
| 1069 |
-
*
|
| 1070 |
*/
|
| 1071 |
function addBibliographyAnchors(html: string, orderedKeys: string[]): string {
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
}
|
| 1080 |
-
);
|
| 1081 |
}
|
| 1082 |
|
| 1083 |
function escapeHtml(str: string): string {
|
|
|
|
| 6 |
*/
|
| 7 |
|
| 8 |
import { generateHTML } from "@tiptap/html";
|
| 9 |
+
import { parseHTML } from "linkedom";
|
| 10 |
import { getServerExtensions } from "./extensions.js";
|
| 11 |
import type { PublishCSS } from "./index.js";
|
| 12 |
|
|
|
|
| 106 |
</script>
|
| 107 |
|
| 108 |
<style>
|
|
|
|
| 109 |
${css.variables}
|
| 110 |
${css.reset}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
${css.editorTokens}
|
| 112 |
${css.base}
|
| 113 |
${css.layout}
|
| 114 |
${css.components}
|
| 115 |
${css.article}
|
| 116 |
${css.print}
|
| 117 |
+
${css.publisher}
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
</style>
|
| 120 |
</head>
|
| 121 |
<body>
|
|
|
|
| 550 |
}
|
| 551 |
|
| 552 |
/**
|
| 553 |
+
* Post-process the raw generateHTML output using linkedom DOM manipulation:
|
|
|
|
| 554 |
* - Transform accordion divs into <details><summary>
|
| 555 |
+
* - Convert inline citation spans into anchor links
|
| 556 |
+
* - Inject bibliography HTML with entry anchors
|
| 557 |
+
* - Transform mermaid divs into <pre class="mermaid">
|
| 558 |
+
* - Transform htmlEmbed divs into iframes
|
| 559 |
+
* - Collect footnotes and append a footnotes section
|
| 560 |
*/
|
| 561 |
function postProcess(html: string, biblioHtml: string, citationData?: CitationData): string {
|
| 562 |
+
const { document } = parseHTML(`<!DOCTYPE html><html><body>${html}</body></html>`);
|
| 563 |
+
|
| 564 |
+
// 1. Accordion: div[data-component="accordion"] -> <details><summary>
|
| 565 |
+
for (const div of [...document.querySelectorAll('div[data-component="accordion"]')]) {
|
| 566 |
+
const title = div.getAttribute("title") || "Details";
|
| 567 |
+
const isOpen = div.getAttribute("open") === "true";
|
| 568 |
+
|
| 569 |
+
const details = document.createElement("details");
|
| 570 |
+
details.setAttribute("data-component", "accordion");
|
| 571 |
+
if (isOpen) details.setAttribute("open", "");
|
| 572 |
+
|
| 573 |
+
const summary = document.createElement("summary");
|
| 574 |
+
summary.textContent = title;
|
| 575 |
+
details.appendChild(summary);
|
| 576 |
+
|
| 577 |
+
const content = document.createElement("div");
|
| 578 |
+
content.className = "accordion-content";
|
| 579 |
+
while (div.firstChild) content.appendChild(div.firstChild);
|
| 580 |
+
details.appendChild(content);
|
| 581 |
+
|
| 582 |
+
div.replaceWith(details);
|
| 583 |
+
}
|
| 584 |
|
| 585 |
+
// 2. Inline citations: span[data-type="citation"] -> <a> links
|
|
|
|
| 586 |
const NUMERIC_STYLES = new Set(["ieee", "vancouver"]);
|
| 587 |
const isNumeric = citationData ? NUMERIC_STYLES.has(citationData.style) : false;
|
| 588 |
const citationKeyOrder: string[] = [];
|
| 589 |
|
| 590 |
+
for (const span of [...document.querySelectorAll('span[data-type="citation"]')]) {
|
| 591 |
+
const key = span.getAttribute("key") || span.getAttribute("data-key");
|
| 592 |
+
if (!key) continue;
|
|
|
|
|
|
|
|
|
|
| 593 |
|
| 594 |
+
if (!citationKeyOrder.includes(key)) citationKeyOrder.push(key);
|
| 595 |
+
const idx = citationKeyOrder.indexOf(key) + 1;
|
| 596 |
|
| 597 |
+
const originalLabel = span.textContent || `[${key}]`;
|
| 598 |
+
const displayLabel = isNumeric ? `[${idx}]` : originalLabel;
|
|
|
|
| 599 |
|
| 600 |
+
const a = document.createElement("a");
|
| 601 |
+
a.setAttribute("href", `#ref-${key}`);
|
| 602 |
+
a.className = "citation-inline";
|
| 603 |
+
a.setAttribute("id", `cite-${key}-${idx}`);
|
| 604 |
+
a.setAttribute("title", key);
|
| 605 |
+
a.textContent = displayLabel;
|
| 606 |
|
| 607 |
+
span.replaceWith(a);
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
// 3. Bibliography: inject pre-formatted HTML into the placeholder
|
| 611 |
+
for (const div of [...document.querySelectorAll('div[data-type="bibliography"]')]) {
|
| 612 |
+
div.removeAttribute("renderedhtml");
|
| 613 |
+
|
| 614 |
+
if (biblioHtml) {
|
| 615 |
+
let enrichedBiblio = biblioHtml;
|
| 616 |
+
if (citationData && citationData.orderedKeys.length > 0) {
|
| 617 |
+
enrichedBiblio = addBibliographyAnchors(biblioHtml, citationData.orderedKeys);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 618 |
}
|
| 619 |
+
div.innerHTML = `<h2 class="bibliography-title">References</h2><div class="bibliography-content">${enrichedBiblio}</div>`;
|
| 620 |
+
} else {
|
| 621 |
+
div.innerHTML = `<p class="bibliography-empty">No citations</p>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
}
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
// 4. Mermaid: div[data-component="mermaid"] -> <pre class="mermaid">
|
| 626 |
+
for (const div of [...document.querySelectorAll('div[data-component="mermaid"]')]) {
|
| 627 |
+
const code = div.getAttribute("code") || div.getAttribute("data-code") || "";
|
| 628 |
+
const pre = document.createElement("pre");
|
| 629 |
+
pre.className = "mermaid";
|
| 630 |
+
pre.textContent = code;
|
| 631 |
+
div.replaceWith(pre);
|
| 632 |
+
}
|
| 633 |
|
| 634 |
+
// 5. HtmlEmbed: div[data-component="htmlEmbed"] -> iframe with srcdoc
|
| 635 |
+
for (const div of [...document.querySelectorAll('div[data-component="htmlEmbed"]')]) {
|
| 636 |
+
const src = div.getAttribute("src") || div.getAttribute("data-src") || "";
|
| 637 |
+
|
| 638 |
+
const container = document.createElement("div");
|
| 639 |
+
container.className = "html-embed-container";
|
| 640 |
+
|
| 641 |
+
const iframe = document.createElement("iframe");
|
| 642 |
+
iframe.setAttribute("srcdoc", "");
|
| 643 |
+
iframe.setAttribute("data-embed-src", src);
|
| 644 |
+
iframe.setAttribute("style", "width:100%;border:none;min-height:400px;");
|
| 645 |
+
iframe.setAttribute("loading", "lazy");
|
| 646 |
+
iframe.setAttribute("sandbox", "allow-scripts");
|
| 647 |
+
container.appendChild(iframe);
|
| 648 |
+
|
| 649 |
+
div.replaceWith(container);
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
// 6. Footnotes: collect span[data-type="footnote"], replace with superscript links
|
| 653 |
+
const footnoteSpans = [...document.querySelectorAll('span[data-type="footnote"]')];
|
| 654 |
+
const footnoteTexts: string[] = [];
|
| 655 |
+
|
| 656 |
+
for (const span of footnoteSpans) {
|
| 657 |
+
const text = span.getAttribute("content") || span.getAttribute("data-content") || span.getAttribute("title") || "";
|
| 658 |
+
footnoteTexts.push(text);
|
| 659 |
+
const idx = footnoteTexts.length;
|
| 660 |
+
|
| 661 |
+
const sup = document.createElement("sup");
|
| 662 |
+
sup.className = "footnote-ref";
|
| 663 |
+
const a = document.createElement("a");
|
| 664 |
+
a.setAttribute("href", `#fn-${idx}`);
|
| 665 |
+
a.setAttribute("id", `fnref-${idx}`);
|
| 666 |
+
a.textContent = `[${idx}]`;
|
| 667 |
+
sup.appendChild(a);
|
| 668 |
+
|
| 669 |
+
span.replaceWith(sup);
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
let result = document.body.innerHTML;
|
| 673 |
+
|
| 674 |
+
if (footnoteTexts.length > 0) {
|
| 675 |
+
const items = footnoteTexts
|
| 676 |
.map((text, i) => `<li id="fn-${i + 1}"><p>${escapeHtml(text)} <a href="#fnref-${i + 1}" class="footnote-backref" aria-label="Back to text">\u21A9</a></p></li>`)
|
| 677 |
.join("\n");
|
| 678 |
+
result += `<section class="footnotes"><h2>Footnotes</h2><ol>${items}</ol></section>`;
|
|
|
|
| 679 |
}
|
| 680 |
|
| 681 |
return result;
|
|
|
|
| 684 |
/**
|
| 685 |
* Add id anchors to bibliography entries so inline citations can link to them.
|
| 686 |
* citation-js outputs entries as <div class="csl-entry"> elements.
|
| 687 |
+
* Uses linkedom for reliable DOM manipulation.
|
| 688 |
*/
|
| 689 |
function addBibliographyAnchors(html: string, orderedKeys: string[]): string {
|
| 690 |
+
const { document } = parseHTML(`<!DOCTYPE html><html><body>${html}</body></html>`);
|
| 691 |
+
const entries = document.querySelectorAll(".csl-entry");
|
| 692 |
+
entries.forEach((entry, idx) => {
|
| 693 |
+
const key = orderedKeys[idx] || `entry-${idx}`;
|
| 694 |
+
entry.setAttribute("id", `ref-${key}`);
|
| 695 |
+
});
|
| 696 |
+
return document.body.innerHTML;
|
|
|
|
|
|
|
| 697 |
}
|
| 698 |
|
| 699 |
function escapeHtml(str: string): string {
|
|
@@ -32,6 +32,7 @@ export interface PublishCSS {
|
|
| 32 |
editorTokens: string;
|
| 33 |
article: string;
|
| 34 |
components: string;
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
/**
|
|
@@ -82,7 +83,6 @@ function loadCSS(): PublishCSS {
|
|
| 82 |
.map((f) => readSafe(join(dir, "components", f)))
|
| 83 |
.join("\n");
|
| 84 |
|
| 85 |
-
// Read all CSS, resolve @custom-media, then split back
|
| 86 |
const variablesRaw = readFileSync(varsPath, "utf-8");
|
| 87 |
const resetRaw = readSafe(join(dir, "_reset.css"));
|
| 88 |
const baseRaw = readFileSync(basePath, "utf-8");
|
|
@@ -90,9 +90,10 @@ function loadCSS(): PublishCSS {
|
|
| 90 |
const printRaw = readSafe(join(dir, "_print.css"));
|
| 91 |
const editorTokensRaw = readSafe(join(dir, "_editor-tokens.css"));
|
| 92 |
const articleRaw = readSafe(join(dir, "article.css"));
|
|
|
|
| 93 |
|
| 94 |
// @custom-media declarations live in _variables.css - resolve across all files
|
| 95 |
-
const allRaw = [variablesRaw, resetRaw, baseRaw, layoutRaw, printRaw, editorTokensRaw, articleRaw, componentsCss].join("\n/* __CSS_BOUNDARY__ */\n");
|
| 96 |
const resolved = resolveCustomMedia(allRaw);
|
| 97 |
const parts = resolved.split("/* __CSS_BOUNDARY__ */");
|
| 98 |
|
|
@@ -105,12 +106,13 @@ function loadCSS(): PublishCSS {
|
|
| 105 |
editorTokens: parts[5]?.trim() ?? "",
|
| 106 |
article: parts[6]?.trim() ?? "",
|
| 107 |
components: parts[7]?.trim() ?? "",
|
|
|
|
| 108 |
};
|
| 109 |
}
|
| 110 |
}
|
| 111 |
|
| 112 |
console.warn("[publish] CSS files not found, tried:", candidates);
|
| 113 |
-
return { variables: "", reset: "", base: "", layout: "", print: "", editorTokens: "", article: "", components: "" };
|
| 114 |
}
|
| 115 |
|
| 116 |
interface AuthorObj {
|
|
@@ -234,6 +236,54 @@ export interface PublishResult {
|
|
| 234 |
error?: string;
|
| 235 |
}
|
| 236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
/**
|
| 238 |
* Run the full publish pipeline for a document.
|
| 239 |
*/
|
|
|
|
| 32 |
editorTokens: string;
|
| 33 |
article: string;
|
| 34 |
components: string;
|
| 35 |
+
publisher: string;
|
| 36 |
}
|
| 37 |
|
| 38 |
/**
|
|
|
|
| 83 |
.map((f) => readSafe(join(dir, "components", f)))
|
| 84 |
.join("\n");
|
| 85 |
|
|
|
|
| 86 |
const variablesRaw = readFileSync(varsPath, "utf-8");
|
| 87 |
const resetRaw = readSafe(join(dir, "_reset.css"));
|
| 88 |
const baseRaw = readFileSync(basePath, "utf-8");
|
|
|
|
| 90 |
const printRaw = readSafe(join(dir, "_print.css"));
|
| 91 |
const editorTokensRaw = readSafe(join(dir, "_editor-tokens.css"));
|
| 92 |
const articleRaw = readSafe(join(dir, "article.css"));
|
| 93 |
+
const publisherRaw = readSafe(join(dir, "_publisher.css"));
|
| 94 |
|
| 95 |
// @custom-media declarations live in _variables.css - resolve across all files
|
| 96 |
+
const allRaw = [variablesRaw, resetRaw, baseRaw, layoutRaw, printRaw, editorTokensRaw, articleRaw, componentsCss, publisherRaw].join("\n/* __CSS_BOUNDARY__ */\n");
|
| 97 |
const resolved = resolveCustomMedia(allRaw);
|
| 98 |
const parts = resolved.split("/* __CSS_BOUNDARY__ */");
|
| 99 |
|
|
|
|
| 106 |
editorTokens: parts[5]?.trim() ?? "",
|
| 107 |
article: parts[6]?.trim() ?? "",
|
| 108 |
components: parts[7]?.trim() ?? "",
|
| 109 |
+
publisher: parts[8]?.trim() ?? "",
|
| 110 |
};
|
| 111 |
}
|
| 112 |
}
|
| 113 |
|
| 114 |
console.warn("[publish] CSS files not found, tried:", candidates);
|
| 115 |
+
return { variables: "", reset: "", base: "", layout: "", print: "", editorTokens: "", article: "", components: "", publisher: "" };
|
| 116 |
}
|
| 117 |
|
| 118 |
interface AuthorObj {
|
|
|
|
| 236 |
error?: string;
|
| 237 |
}
|
| 238 |
|
| 239 |
+
/**
|
| 240 |
+
* Render a preview HTML of the document without saving or uploading anything.
|
| 241 |
+
* Useful for testing the publisher pipeline.
|
| 242 |
+
*/
|
| 243 |
+
export async function previewDocument(docName: string): Promise<{ html: string } | { error: string }> {
|
| 244 |
+
const p = docPath(docName);
|
| 245 |
+
if (!existsSync(p)) return { error: "Document not found" };
|
| 246 |
+
|
| 247 |
+
const ydoc = new Y.Doc();
|
| 248 |
+
const data = readFileSync(p);
|
| 249 |
+
Y.applyUpdate(ydoc, new Uint8Array(data));
|
| 250 |
+
|
| 251 |
+
const { json, frontmatter, authors, affiliations } = extractFromYDoc(ydoc);
|
| 252 |
+
const citationData = extractCitationsFromYDoc(ydoc, json);
|
| 253 |
+
|
| 254 |
+
const meta: PublishMeta = {
|
| 255 |
+
title: (frontmatter.title as string) || docName,
|
| 256 |
+
subtitle: (frontmatter.subtitle as string) || undefined,
|
| 257 |
+
description: (frontmatter.description as string) || "",
|
| 258 |
+
authors: authors.map((a) => ({
|
| 259 |
+
name: a.name,
|
| 260 |
+
url: a.url,
|
| 261 |
+
affiliationIndices: a.affiliations || [],
|
| 262 |
+
affiliationNames: (a.affiliations || [])
|
| 263 |
+
.map((idx) => affiliations[idx - 1]?.name)
|
| 264 |
+
.filter(Boolean) as string[],
|
| 265 |
+
})),
|
| 266 |
+
affiliations: affiliations.map((a) => ({ name: a.name, url: a.url })),
|
| 267 |
+
date: (frontmatter.published as string) || (frontmatter.date as string) || new Date().toISOString(),
|
| 268 |
+
doi: (frontmatter.doi as string) || undefined,
|
| 269 |
+
licence: (frontmatter.licence as string) || (frontmatter.license as string) || undefined,
|
| 270 |
+
};
|
| 271 |
+
|
| 272 |
+
const css = loadCSS();
|
| 273 |
+
|
| 274 |
+
let biblioHtml = "";
|
| 275 |
+
if (citationData.entries.length > 0) {
|
| 276 |
+
try {
|
| 277 |
+
biblioHtml = await formatBibliographyServer(citationData.entries, citationData.style);
|
| 278 |
+
} catch (err) {
|
| 279 |
+
console.error("[preview] bibliography formatting failed:", err);
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
const html = renderArticleHTML(json, meta, css, citationData, biblioHtml);
|
| 284 |
+
return { html };
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
/**
|
| 288 |
* Run the full publish pipeline for a document.
|
| 289 |
*/
|
|
@@ -23,7 +23,7 @@ import {
|
|
| 23 |
flushAll,
|
| 24 |
} from "./hf-storage.js";
|
| 25 |
import { resolveUser, extractToken, isOAuthEnabled, handleOAuthAuthorize, handleOAuthCallback } from "./auth.js";
|
| 26 |
-
import { publishDocument } from "./publisher/index.js";
|
| 27 |
import { DATA_DIR, docPath, sanitizeName } from "./utils.js";
|
| 28 |
|
| 29 |
const PORT = parseInt(process.env.PORT || "8080", 10);
|
|
@@ -222,6 +222,42 @@ app.post("/api/chat", handleChat);
|
|
| 222 |
|
| 223 |
app.use("/api/citations", citationsRouter);
|
| 224 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
// ---------- Publish ----------
|
| 226 |
|
| 227 |
app.post("/api/publish", async (req, res) => {
|
|
|
|
| 23 |
flushAll,
|
| 24 |
} from "./hf-storage.js";
|
| 25 |
import { resolveUser, extractToken, isOAuthEnabled, handleOAuthAuthorize, handleOAuthCallback } from "./auth.js";
|
| 26 |
+
import { publishDocument, previewDocument } from "./publisher/index.js";
|
| 27 |
import { DATA_DIR, docPath, sanitizeName } from "./utils.js";
|
| 28 |
|
| 29 |
const PORT = parseInt(process.env.PORT || "8080", 10);
|
|
|
|
| 222 |
|
| 223 |
app.use("/api/citations", citationsRouter);
|
| 224 |
|
| 225 |
+
// ---------- Preview (render without saving) ----------
|
| 226 |
+
|
| 227 |
+
app.get("/api/preview/:docName", async (req, res) => {
|
| 228 |
+
const docName = req.params.docName || DEFAULT_DOC_NAME;
|
| 229 |
+
|
| 230 |
+
if (oauthEnabled) {
|
| 231 |
+
const token = extractToken(req.headers.cookie);
|
| 232 |
+
const user = await resolveUser(token);
|
| 233 |
+
if (!user || !user.canEdit) {
|
| 234 |
+
res.status(403).json({ error: "Unauthorized" });
|
| 235 |
+
return;
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
try {
|
| 240 |
+
// Flush the live Hocuspocus doc to disk first
|
| 241 |
+
const conn = await hocuspocus.openDirectConnection(docName);
|
| 242 |
+
if (conn.document) {
|
| 243 |
+
const update = Y.encodeStateAsUpdate(conn.document);
|
| 244 |
+
writeFileSync(docPath(docName), Buffer.from(update));
|
| 245 |
+
}
|
| 246 |
+
await conn.disconnect();
|
| 247 |
+
|
| 248 |
+
const result = await previewDocument(docName);
|
| 249 |
+
if ("error" in result) {
|
| 250 |
+
res.status(404).json({ error: result.error });
|
| 251 |
+
return;
|
| 252 |
+
}
|
| 253 |
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
| 254 |
+
res.send(result.html);
|
| 255 |
+
} catch (err: any) {
|
| 256 |
+
console.error("[preview] error:", err);
|
| 257 |
+
res.status(500).json({ error: err.message || "Preview failed" });
|
| 258 |
+
}
|
| 259 |
+
});
|
| 260 |
+
|
| 261 |
// ---------- Publish ----------
|
| 262 |
|
| 263 |
app.post("/api/publish", async (req, res) => {
|
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ---------------------------------------------------------------------------
|
| 2 |
+
// Shared component definitions
|
| 3 |
+
//
|
| 4 |
+
// Single source of truth for the MDX component registry.
|
| 5 |
+
// Both the frontend editor and the backend publisher import this file.
|
| 6 |
+
// ---------------------------------------------------------------------------
|
| 7 |
+
|
| 8 |
+
export interface SharedField {
|
| 9 |
+
name: string;
|
| 10 |
+
type: "string" | "boolean" | "select";
|
| 11 |
+
default?: unknown;
|
| 12 |
+
options?: string[];
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export interface SharedComponentDef {
|
| 16 |
+
name: string;
|
| 17 |
+
kind: "wrapper" | "atomic";
|
| 18 |
+
fields: SharedField[];
|
| 19 |
+
/** ProseMirror content expression (wrapper only, defaults to "block+") */
|
| 20 |
+
content?: string;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export const SHARED_COMPONENT_DEFS: SharedComponentDef[] = [
|
| 24 |
+
{
|
| 25 |
+
name: "accordion",
|
| 26 |
+
kind: "wrapper",
|
| 27 |
+
fields: [
|
| 28 |
+
{ name: "title", type: "string", default: "Details" },
|
| 29 |
+
{ name: "open", type: "boolean", default: false },
|
| 30 |
+
],
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
name: "note",
|
| 34 |
+
kind: "wrapper",
|
| 35 |
+
fields: [
|
| 36 |
+
{ name: "title", type: "string", default: "" },
|
| 37 |
+
{ name: "emoji", type: "string", default: "" },
|
| 38 |
+
{ name: "variant", type: "select", default: "neutral", options: ["neutral", "info", "success", "danger"] },
|
| 39 |
+
],
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
name: "quoteBlock",
|
| 43 |
+
kind: "wrapper",
|
| 44 |
+
fields: [
|
| 45 |
+
{ name: "author", type: "string", default: "" },
|
| 46 |
+
{ name: "source", type: "string", default: "" },
|
| 47 |
+
],
|
| 48 |
+
},
|
| 49 |
+
{ name: "wide", kind: "wrapper", fields: [] },
|
| 50 |
+
{ name: "fullWidth", kind: "wrapper", fields: [] },
|
| 51 |
+
{ name: "sidenote", kind: "wrapper", fields: [] },
|
| 52 |
+
{
|
| 53 |
+
name: "reference",
|
| 54 |
+
kind: "wrapper",
|
| 55 |
+
fields: [
|
| 56 |
+
{ name: "id", type: "string", default: "" },
|
| 57 |
+
{ name: "caption", type: "string", default: "" },
|
| 58 |
+
],
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
name: "htmlEmbed",
|
| 62 |
+
kind: "atomic",
|
| 63 |
+
fields: [
|
| 64 |
+
{ name: "src", type: "string", default: "" },
|
| 65 |
+
{ name: "title", type: "string", default: "" },
|
| 66 |
+
{ name: "desc", type: "string", default: "" },
|
| 67 |
+
{ name: "wide", type: "boolean", default: false },
|
| 68 |
+
{ name: "downloadable", type: "boolean", default: false },
|
| 69 |
+
],
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
name: "hfUser",
|
| 73 |
+
kind: "atomic",
|
| 74 |
+
fields: [
|
| 75 |
+
{ name: "username", type: "string", default: "" },
|
| 76 |
+
{ name: "name", type: "string", default: "" },
|
| 77 |
+
{ name: "url", type: "string", default: "" },
|
| 78 |
+
],
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
name: "rawHtml",
|
| 82 |
+
kind: "atomic",
|
| 83 |
+
fields: [
|
| 84 |
+
{ name: "html", type: "string", default: "" },
|
| 85 |
+
],
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
name: "mermaid",
|
| 89 |
+
kind: "atomic",
|
| 90 |
+
fields: [
|
| 91 |
+
{ name: "code", type: "string", default: "" },
|
| 92 |
+
],
|
| 93 |
+
},
|
| 94 |
+
];
|
|
@@ -0,0 +1,475 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
| 2 |
+
|
| 3 |
+
exports[`snapshot - full render > matches snapshot for a typical article 1`] = `
|
| 4 |
+
"<!DOCTYPE html>
|
| 5 |
+
<html lang="en" data-theme="dark">
|
| 6 |
+
<head>
|
| 7 |
+
<meta charset="UTF-8">
|
| 8 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 9 |
+
<title>Test Article</title>
|
| 10 |
+
<meta name="description" content="A test article">
|
| 11 |
+
<meta name="author" content="Alice">
|
| 12 |
+
|
| 13 |
+
<!-- Open Graph -->
|
| 14 |
+
<meta property="og:type" content="article">
|
| 15 |
+
<meta property="og:title" content="Test Article">
|
| 16 |
+
<meta property="og:description" content="A test article">
|
| 17 |
+
|
| 18 |
+
<meta property="article:published_time" content="2025-01-01">
|
| 19 |
+
<meta property="article:author" content="Alice">
|
| 20 |
+
|
| 21 |
+
<!-- Twitter Card -->
|
| 22 |
+
<meta name="twitter:card" content="summary_large_image">
|
| 23 |
+
<meta name="twitter:title" content="Test Article">
|
| 24 |
+
<meta name="twitter:description" content="A test article">
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
<!-- KaTeX CSS -->
|
| 28 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css"
|
| 29 |
+
integrity="sha384-zh0CIslj3dQfMxK3pZnPJAb3bprHitCE2vBz9TyOTICAn3fso5GYa90qPNMBclov"
|
| 30 |
+
crossorigin="anonymous">
|
| 31 |
+
|
| 32 |
+
<!-- highlight.js for code syntax highlighting -->
|
| 33 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css">
|
| 34 |
+
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
| 35 |
+
<script>hljs.highlightAll();</script>
|
| 36 |
+
|
| 37 |
+
<!-- Mermaid (renders <pre class="mermaid"> blocks) -->
|
| 38 |
+
<script type="module">
|
| 39 |
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
| 40 |
+
mermaid.initialize({ startOnLoad: true, theme: 'neutral' });
|
| 41 |
+
</script>
|
| 42 |
+
|
| 43 |
+
<style>
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
</style>
|
| 55 |
+
</head>
|
| 56 |
+
<body>
|
| 57 |
+
<button id="theme-toggle" aria-label="Toggle color theme">
|
| 58 |
+
<span class="icon-wrapper">
|
| 59 |
+
<svg class="icon icon--sun" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 60 |
+
<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="23"/><line x1="1" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="23" y2="12"/><line x1="4.22" y1="4.22" x2="6.34" y2="6.34"/><line x1="17.66" y1="17.66" x2="19.78" y2="19.78"/><line x1="4.22" y1="19.78" x2="6.34" y2="17.66"/><line x1="17.66" y1="6.34" x2="19.78" y2="4.22"/>
|
| 61 |
+
</svg>
|
| 62 |
+
<svg class="icon icon--moon" style="opacity:0" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 63 |
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
| 64 |
+
</svg>
|
| 65 |
+
</span>
|
| 66 |
+
</button>
|
| 67 |
+
|
| 68 |
+
<!-- Mobile TOC toggle -->
|
| 69 |
+
<button class="toc-mobile-toggle" aria-label="Open table of contents" aria-expanded="false">
|
| 70 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 71 |
+
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
|
| 72 |
+
</svg>
|
| 73 |
+
</button>
|
| 74 |
+
<div class="toc-mobile-backdrop" aria-hidden="true"></div>
|
| 75 |
+
<aside class="toc-mobile-sidebar" aria-label="Table of Contents">
|
| 76 |
+
<div class="toc-mobile-sidebar__header">
|
| 77 |
+
<span class="toc-mobile-sidebar__title">Table of Contents</span>
|
| 78 |
+
<button class="toc-mobile-sidebar__close" aria-label="Close">
|
| 79 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 80 |
+
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
| 81 |
+
</svg>
|
| 82 |
+
</button>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="toc-mobile-sidebar__body" id="toc-mobile-placeholder"></div>
|
| 85 |
+
</aside>
|
| 86 |
+
|
| 87 |
+
<!-- Hero -->
|
| 88 |
+
<section class="hero">
|
| 89 |
+
<h1 class="hero-title">Test Article</h1>
|
| 90 |
+
|
| 91 |
+
</section>
|
| 92 |
+
|
| 93 |
+
<!-- Meta bar -->
|
| 94 |
+
<header class="meta"><div class="meta-container"><div class="meta-container-cell"><h3>Authors</h3><ul class="authors"><li>Alice</li></ul></div><div class="meta-container-cell"><h3>Affiliations</h3><p>MIT</p></div><div class="meta-container-cell"><h3>Published</h3><p>January 1, 2025</p></div></div></header>
|
| 95 |
+
|
| 96 |
+
<section class="content-grid">
|
| 97 |
+
<nav class="table-of-contents" aria-label="Table of Contents">
|
| 98 |
+
<div class="title">Table of Contents</div>
|
| 99 |
+
<div id="toc-placeholder"></div>
|
| 100 |
+
</nav>
|
| 101 |
+
|
| 102 |
+
<main>
|
| 103 |
+
<div class="tiptap">
|
| 104 |
+
<h2>Introduction</h2><p>This is a test article with a <a title="ref1" id="cite-ref1-1" class="citation-inline" href="#ref-ref1">Ref (2024)</a> citation.</p><p>A footnote here<sup class="footnote-ref"><a id="fnref-1" href="#fn-1">[1]</a></sup></p><div data-type="bibliography" class="bibliography-block"><h2 class="bibliography-title">References</h2><div class="bibliography-content"><div id="ref-ref1" class="csl-entry">Reference One. 2024.</div></div></div><section class="footnotes"><h2>Footnotes</h2><ol><li id="fn-1"><p>Important detail <a href="#fnref-1" class="footnote-backref" aria-label="Back to text">↩</a></p></li></ol></section>
|
| 105 |
+
</div>
|
| 106 |
+
</main>
|
| 107 |
+
</section>
|
| 108 |
+
|
| 109 |
+
<!-- Footer -->
|
| 110 |
+
<footer class="footer"><div class="footer-inner">
|
| 111 |
+
<section class="citation-block">
|
| 112 |
+
<p class="footer-heading" role="heading" aria-level="2">Citation</p>
|
| 113 |
+
<p>For attribution in academic contexts, please cite this work as</p>
|
| 114 |
+
<pre class="citation short">Alice (2025). "Test Article".</pre>
|
| 115 |
+
<p>BibTeX citation</p>
|
| 116 |
+
<pre class="citation long">@misc{alice2025_test_article,
|
| 117 |
+
title={Test Article},
|
| 118 |
+
author={Alice},
|
| 119 |
+
year={2025}
|
| 120 |
+
}</pre>
|
| 121 |
+
</section>
|
| 122 |
+
<section class="references-block"></section>
|
| 123 |
+
<div class="template-credit">
|
| 124 |
+
<p>Made with ❤️ with <a href="https://huggingface.co/spaces/tfrere/research-article-template" target="_blank" rel="noopener noreferrer">research article template</a></p>
|
| 125 |
+
</div></div></footer>
|
| 126 |
+
|
| 127 |
+
<!-- Image lightbox -->
|
| 128 |
+
<dialog class="lightbox" id="lightbox">
|
| 129 |
+
<img id="lightbox-img" src="" alt="">
|
| 130 |
+
</dialog>
|
| 131 |
+
|
| 132 |
+
<script>
|
| 133 |
+
(function() {
|
| 134 |
+
// Theme toggle (matches template ThemeToggle.astro)
|
| 135 |
+
var btn = document.getElementById('theme-toggle');
|
| 136 |
+
var sunIcon = btn && btn.querySelector('.icon--sun');
|
| 137 |
+
var moonIcon = btn && btn.querySelector('.icon--moon');
|
| 138 |
+
var wrapper = btn && btn.querySelector('.icon-wrapper');
|
| 139 |
+
var media = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
|
| 140 |
+
var prefersDark = media && media.matches;
|
| 141 |
+
var saved = localStorage.getItem('theme');
|
| 142 |
+
|
| 143 |
+
function applyTheme(mode) {
|
| 144 |
+
document.documentElement.dataset.theme = mode;
|
| 145 |
+
if (sunIcon && moonIcon) {
|
| 146 |
+
sunIcon.style.opacity = mode === 'dark' ? '0' : '1';
|
| 147 |
+
moonIcon.style.opacity = mode === 'dark' ? '1' : '0';
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
applyTheme(saved || (prefersDark ? 'dark' : 'light'));
|
| 151 |
+
requestAnimationFrame(function() { if (wrapper) wrapper.classList.add('animated'); });
|
| 152 |
+
|
| 153 |
+
if (!saved && media) {
|
| 154 |
+
var syncSystem = function(e) { applyTheme(e.matches ? 'dark' : 'light'); };
|
| 155 |
+
if (media.addEventListener) media.addEventListener('change', syncSystem);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
if (btn) {
|
| 159 |
+
btn.addEventListener('click', function() {
|
| 160 |
+
var next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
|
| 161 |
+
localStorage.setItem('theme', next);
|
| 162 |
+
if (wrapper) {
|
| 163 |
+
var cls = next === 'dark' ? 'spin-cw' : 'spin-ccw';
|
| 164 |
+
wrapper.classList.remove('spin-cw', 'spin-ccw');
|
| 165 |
+
void wrapper.offsetWidth;
|
| 166 |
+
wrapper.classList.add(cls);
|
| 167 |
+
wrapper.addEventListener('animationend', function() { wrapper.classList.remove(cls); }, { once: true });
|
| 168 |
+
}
|
| 169 |
+
applyTheme(next);
|
| 170 |
+
});
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// Lightbox
|
| 174 |
+
document.querySelectorAll('.tiptap img').forEach(function(img) {
|
| 175 |
+
img.style.cursor = 'zoom-in';
|
| 176 |
+
img.addEventListener('click', function() {
|
| 177 |
+
var lbImg = document.getElementById('lightbox-img');
|
| 178 |
+
var lbDlg = document.getElementById('lightbox');
|
| 179 |
+
if (lbImg) lbImg.src = img.src;
|
| 180 |
+
if (lbDlg && lbDlg.showModal) lbDlg.showModal();
|
| 181 |
+
});
|
| 182 |
+
});
|
| 183 |
+
var lb = document.getElementById('lightbox');
|
| 184 |
+
if (lb) lb.addEventListener('click', function(e) { if (e.target === this) this.close(); });
|
| 185 |
+
|
| 186 |
+
// Glossary
|
| 187 |
+
document.querySelectorAll('[data-type="glossary"]').forEach(function(el) { el.setAttribute('tabindex', '0'); });
|
| 188 |
+
|
| 189 |
+
// ---- Table of Contents ----
|
| 190 |
+
var holder = document.getElementById('toc-placeholder');
|
| 191 |
+
var holderMobile = document.getElementById('toc-mobile-placeholder');
|
| 192 |
+
var articleRoot = document.querySelector('.content-grid main');
|
| 193 |
+
if (!articleRoot) return;
|
| 194 |
+
var headings = articleRoot.querySelectorAll('h2, h3, h4');
|
| 195 |
+
if (!headings.length) return;
|
| 196 |
+
var headingsArr = Array.from(headings);
|
| 197 |
+
|
| 198 |
+
// Unique IDs
|
| 199 |
+
var usedIds = {};
|
| 200 |
+
var slugify = function(s) { return String(s||'').toLowerCase().trim().replace(/\\s+/g,'_').replace(/[^a-z0-9_-]/g,''); };
|
| 201 |
+
headingsArr.forEach(function(h) {
|
| 202 |
+
var id = (h.id||'').trim() || slugify(h.textContent) || 'section';
|
| 203 |
+
var c = id, n = 2;
|
| 204 |
+
while (usedIds[c]) c = id+'-'+(n++);
|
| 205 |
+
if (h.id !== c) h.id = c;
|
| 206 |
+
usedIds[c] = true;
|
| 207 |
+
});
|
| 208 |
+
|
| 209 |
+
// Build nested nav with data-heading-idx
|
| 210 |
+
var nav = document.createElement('nav');
|
| 211 |
+
var ulStack = [document.createElement('ul')];
|
| 212 |
+
nav.appendChild(ulStack[0]);
|
| 213 |
+
var levelOf = function(tag) { return tag==='H2'?2:tag==='H3'?3:4; };
|
| 214 |
+
var prev = 2, hIdx = 0;
|
| 215 |
+
headingsArr.forEach(function(h) {
|
| 216 |
+
var lvl = levelOf(h.tagName);
|
| 217 |
+
while (lvl > prev) { var ul = document.createElement('ul'); var last = ulStack[ulStack.length-1].lastElementChild; if (last) last.appendChild(ul); ulStack.push(ul); prev++; }
|
| 218 |
+
while (lvl < prev) { ulStack.pop(); prev--; }
|
| 219 |
+
var li = document.createElement('li');
|
| 220 |
+
var a = document.createElement('a');
|
| 221 |
+
a.href = '#'+h.id; a.textContent = h.textContent;
|
| 222 |
+
li.appendChild(a);
|
| 223 |
+
li.setAttribute('data-heading-idx', String(hIdx++));
|
| 224 |
+
ulStack[ulStack.length-1].appendChild(li);
|
| 225 |
+
});
|
| 226 |
+
|
| 227 |
+
if (holder) holder.appendChild(nav);
|
| 228 |
+
var navClone = nav.cloneNode(true);
|
| 229 |
+
if (holderMobile) holderMobile.appendChild(navClone);
|
| 230 |
+
|
| 231 |
+
// Mark navs as collapsible
|
| 232 |
+
nav.classList.add('toc-collapsible');
|
| 233 |
+
navClone.classList.add('toc-collapsible');
|
| 234 |
+
|
| 235 |
+
var allLinks = [].concat(
|
| 236 |
+
holder ? Array.from(holder.querySelectorAll('a')) : [],
|
| 237 |
+
holderMobile ? Array.from(holderMobile.querySelectorAll('a')) : []
|
| 238 |
+
);
|
| 239 |
+
|
| 240 |
+
// Click handler for smooth scroll with offset
|
| 241 |
+
var SCROLL_OFFSET = 80;
|
| 242 |
+
allLinks.forEach(function(link) {
|
| 243 |
+
link.addEventListener('click', function(e) {
|
| 244 |
+
var href = link.getAttribute('href');
|
| 245 |
+
if (!href || href.charAt(0) !== '#') return;
|
| 246 |
+
var el = document.getElementById(href.slice(1));
|
| 247 |
+
if (!el) return;
|
| 248 |
+
e.preventDefault();
|
| 249 |
+
var top = el.getBoundingClientRect().top + window.scrollY - SCROLL_OFFSET;
|
| 250 |
+
window.scrollTo({ top: top, behavior: 'smooth' });
|
| 251 |
+
history.pushState(null, '', href);
|
| 252 |
+
});
|
| 253 |
+
});
|
| 254 |
+
|
| 255 |
+
// ---- Auto-collapse logic ----
|
| 256 |
+
var isMobile = window.matchMedia('(max-width: 1100px)');
|
| 257 |
+
|
| 258 |
+
function getItemsWithChildren(navEl) {
|
| 259 |
+
if (!navEl) return [];
|
| 260 |
+
return Array.from(navEl.querySelectorAll('li[data-heading-idx]')).filter(function(li) {
|
| 261 |
+
return li.querySelector(':scope > ul');
|
| 262 |
+
});
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
function getAncestorIndices(items, targetIdx) {
|
| 266 |
+
var toExpand = {};
|
| 267 |
+
var activeLi = null;
|
| 268 |
+
function find(li) {
|
| 269 |
+
if (Number(li.getAttribute('data-heading-idx')) === targetIdx) return li;
|
| 270 |
+
var ul = li.querySelector(':scope > ul');
|
| 271 |
+
if (!ul) return null;
|
| 272 |
+
var children = ul.querySelectorAll(':scope > li[data-heading-idx]');
|
| 273 |
+
for (var i = 0; i < children.length; i++) { var f = find(children[i]); if (f) return f; }
|
| 274 |
+
return null;
|
| 275 |
+
}
|
| 276 |
+
for (var i = 0; i < items.length; i++) { activeLi = find(items[i]); if (activeLi) break; }
|
| 277 |
+
if (!activeLi) return toExpand;
|
| 278 |
+
toExpand[targetIdx] = true;
|
| 279 |
+
var cur = activeLi;
|
| 280 |
+
while (cur) {
|
| 281 |
+
var parent = cur.parentElement ? cur.parentElement.closest('li[data-heading-idx]') : null;
|
| 282 |
+
if (parent) { toExpand[Number(parent.getAttribute('data-heading-idx'))] = true; cur = parent; }
|
| 283 |
+
else break;
|
| 284 |
+
}
|
| 285 |
+
return toExpand;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
function applyCollapseState(navEl, activeIdx) {
|
| 289 |
+
if (!navEl) return;
|
| 290 |
+
var items = getItemsWithChildren(navEl);
|
| 291 |
+
var ancestors = getAncestorIndices(items, activeIdx);
|
| 292 |
+
|
| 293 |
+
items.forEach(function(li) {
|
| 294 |
+
var sub = li.querySelector(':scope > ul');
|
| 295 |
+
if (!sub) return;
|
| 296 |
+
var idx = Number(li.getAttribute('data-heading-idx'));
|
| 297 |
+
var allDesc = li.querySelectorAll('li[data-heading-idx]');
|
| 298 |
+
var related = [idx];
|
| 299 |
+
allDesc.forEach(function(d) { related.push(Number(d.getAttribute('data-heading-idx'))); });
|
| 300 |
+
var shouldExpand = related.some(function(i) { return ancestors[i]; });
|
| 301 |
+
|
| 302 |
+
if (shouldExpand) {
|
| 303 |
+
li.classList.remove('collapsed');
|
| 304 |
+
sub.style.height = 'auto';
|
| 305 |
+
} else {
|
| 306 |
+
li.classList.add('collapsed');
|
| 307 |
+
sub.style.height = '0px';
|
| 308 |
+
}
|
| 309 |
+
});
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
function expandAll(navEl) {
|
| 313 |
+
if (!navEl) return;
|
| 314 |
+
getItemsWithChildren(navEl).forEach(function(li) {
|
| 315 |
+
li.classList.remove('collapsed');
|
| 316 |
+
var sub = li.querySelector(':scope > ul');
|
| 317 |
+
if (sub) sub.style.height = 'auto';
|
| 318 |
+
});
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
// Initial state: collapse all except first section
|
| 322 |
+
function initCollapse() {
|
| 323 |
+
if (isMobile.matches) {
|
| 324 |
+
expandAll(holder ? holder.querySelector('nav') : null);
|
| 325 |
+
expandAll(holderMobile ? holderMobile.querySelector('nav') : null);
|
| 326 |
+
} else {
|
| 327 |
+
applyCollapseState(holder ? holder.querySelector('nav') : null, 0);
|
| 328 |
+
applyCollapseState(holderMobile ? holderMobile.querySelector('nav') : null, 0);
|
| 329 |
+
}
|
| 330 |
+
}
|
| 331 |
+
initCollapse();
|
| 332 |
+
|
| 333 |
+
isMobile.addEventListener('change', function() {
|
| 334 |
+
if (isMobile.matches) {
|
| 335 |
+
expandAll(holder ? holder.querySelector('nav') : null);
|
| 336 |
+
expandAll(holderMobile ? holderMobile.querySelector('nav') : null);
|
| 337 |
+
} else {
|
| 338 |
+
applyCollapseState(holder ? holder.querySelector('nav') : null, prevActiveIdx);
|
| 339 |
+
applyCollapseState(holderMobile ? holderMobile.querySelector('nav') : null, prevActiveIdx);
|
| 340 |
+
}
|
| 341 |
+
});
|
| 342 |
+
|
| 343 |
+
// ---- Scroll tracking ----
|
| 344 |
+
var OFFSET = 60, lastScrollTime = 0, prevActiveIdx = 0;
|
| 345 |
+
|
| 346 |
+
function onScroll() {
|
| 347 |
+
var now = performance.now();
|
| 348 |
+
if (now - lastScrollTime < 50) return;
|
| 349 |
+
lastScrollTime = now;
|
| 350 |
+
requestAnimationFrame(function() {
|
| 351 |
+
var activeIdx = -1, activeId = null;
|
| 352 |
+
for (var i = headingsArr.length - 1; i >= 0; i--) {
|
| 353 |
+
if (headingsArr[i].getBoundingClientRect().top - OFFSET <= 0) {
|
| 354 |
+
activeIdx = i; activeId = headingsArr[i].id; break;
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
allLinks.forEach(function(l) {
|
| 358 |
+
if (activeId && l.getAttribute('href') === '#'+activeId) l.classList.add('active');
|
| 359 |
+
else l.classList.remove('active');
|
| 360 |
+
});
|
| 361 |
+
if (activeIdx !== prevActiveIdx && activeIdx >= 0 && !isMobile.matches) {
|
| 362 |
+
prevActiveIdx = activeIdx;
|
| 363 |
+
applyCollapseState(holder ? holder.querySelector('nav') : null, activeIdx);
|
| 364 |
+
}
|
| 365 |
+
if (activeIdx >= 0) prevActiveIdx = activeIdx;
|
| 366 |
+
});
|
| 367 |
+
}
|
| 368 |
+
window.addEventListener('scroll', onScroll, { passive: true });
|
| 369 |
+
onScroll();
|
| 370 |
+
|
| 371 |
+
// ---- Mobile sidebar ----
|
| 372 |
+
var sidebar = document.querySelector('.toc-mobile-sidebar');
|
| 373 |
+
var backdrop = document.querySelector('.toc-mobile-backdrop');
|
| 374 |
+
var toggleBtn = document.querySelector('.toc-mobile-toggle');
|
| 375 |
+
var closeBtn = document.querySelector('.toc-mobile-sidebar__close');
|
| 376 |
+
|
| 377 |
+
function openSidebar() {
|
| 378 |
+
sidebar.classList.add('open'); backdrop.classList.add('open');
|
| 379 |
+
toggleBtn.setAttribute('aria-expanded','true');
|
| 380 |
+
document.body.style.overflow = 'hidden';
|
| 381 |
+
requestAnimationFrame(function() {
|
| 382 |
+
var active = sidebar.querySelector('a.active');
|
| 383 |
+
if (active) {
|
| 384 |
+
var body = sidebar.querySelector('.toc-mobile-sidebar__body');
|
| 385 |
+
if (body) body.scrollTop = Math.max(0, active.offsetTop - body.offsetTop - body.clientHeight/3);
|
| 386 |
+
}
|
| 387 |
+
});
|
| 388 |
+
}
|
| 389 |
+
function closeSidebar() {
|
| 390 |
+
sidebar.classList.remove('open'); backdrop.classList.remove('open');
|
| 391 |
+
toggleBtn.setAttribute('aria-expanded','false');
|
| 392 |
+
document.body.style.overflow = '';
|
| 393 |
+
}
|
| 394 |
+
if (toggleBtn) toggleBtn.addEventListener('click', openSidebar);
|
| 395 |
+
if (closeBtn) closeBtn.addEventListener('click', closeSidebar);
|
| 396 |
+
if (backdrop) backdrop.addEventListener('click', closeSidebar);
|
| 397 |
+
if (holderMobile) holderMobile.addEventListener('click', function(e) { if (e.target.closest && e.target.closest('a')) closeSidebar(); });
|
| 398 |
+
document.addEventListener('keydown', function(e) { if (e.key==='Escape' && sidebar.classList.contains('open')) closeSidebar(); });
|
| 399 |
+
|
| 400 |
+
// ---- Footer: move references & footnotes ----
|
| 401 |
+
(function() {
|
| 402 |
+
var footer = document.querySelector('footer.footer');
|
| 403 |
+
if (!footer) return;
|
| 404 |
+
var target = footer.querySelector('.references-block');
|
| 405 |
+
if (!target) return;
|
| 406 |
+
var contentRoot = document.querySelector('.content-grid main') || document.body;
|
| 407 |
+
|
| 408 |
+
var ensureHeading = function(text) {
|
| 409 |
+
var exists = Array.from(target.children).some(function(c) {
|
| 410 |
+
return c.classList.contains('footer-heading') && c.textContent.trim().toLowerCase() === text.toLowerCase();
|
| 411 |
+
});
|
| 412 |
+
if (!exists) {
|
| 413 |
+
var h = document.createElement('p');
|
| 414 |
+
h.className = 'footer-heading';
|
| 415 |
+
h.setAttribute('role', 'heading');
|
| 416 |
+
h.setAttribute('aria-level', '2');
|
| 417 |
+
h.textContent = text;
|
| 418 |
+
target.appendChild(h);
|
| 419 |
+
}
|
| 420 |
+
};
|
| 421 |
+
|
| 422 |
+
var moveIntoFooter = function(el, headingText) {
|
| 423 |
+
if (!el) return false;
|
| 424 |
+
var firstH = el.querySelector(':scope > h1, :scope > h2, :scope > h3, :scope > .footer-heading, :scope > .bibliography-title');
|
| 425 |
+
if (firstH) {
|
| 426 |
+
var t = (firstH.textContent || '').trim().toLowerCase();
|
| 427 |
+
if (t === headingText.toLowerCase() || t.includes('reference') || t.includes('bibliograph')) firstH.remove();
|
| 428 |
+
}
|
| 429 |
+
ensureHeading(headingText);
|
| 430 |
+
target.appendChild(el);
|
| 431 |
+
return true;
|
| 432 |
+
};
|
| 433 |
+
|
| 434 |
+
var findAllOutsideFooter = function(selectors) {
|
| 435 |
+
var results = [];
|
| 436 |
+
for (var i = 0; i < selectors.length; i++) {
|
| 437 |
+
var els = contentRoot.querySelectorAll(selectors[i]);
|
| 438 |
+
els.forEach(function(el) { if (!footer.contains(el) && results.indexOf(el) === -1) results.push(el); });
|
| 439 |
+
}
|
| 440 |
+
return results;
|
| 441 |
+
};
|
| 442 |
+
|
| 443 |
+
var findFirst = function(selectors) { var a = findAllOutsideFooter(selectors); return a.length ? a[0] : null; };
|
| 444 |
+
|
| 445 |
+
var run = function() {
|
| 446 |
+
if (footer.dataset.processed === 'true') return;
|
| 447 |
+
var refs = findAllOutsideFooter(['[data-type="bibliography"]', '#bibliography-references-list', '#references', '#refs', '.bibliography']);
|
| 448 |
+
var notes = findFirst(['.footnotes', 'section.footnotes', 'div.footnotes']);
|
| 449 |
+
var moved = false;
|
| 450 |
+
refs.forEach(function(el) { if (moveIntoFooter(el, 'References')) moved = true; });
|
| 451 |
+
if (moveIntoFooter(notes, 'Footnotes')) moved = true;
|
| 452 |
+
if (moved) footer.dataset.processed = 'true';
|
| 453 |
+
};
|
| 454 |
+
|
| 455 |
+
run();
|
| 456 |
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run, { once: true });
|
| 457 |
+
window.addEventListener('load', function() { setTimeout(run, 100); }, { once: true });
|
| 458 |
+
setTimeout(run, 300);
|
| 459 |
+
})();
|
| 460 |
+
|
| 461 |
+
// Hash navigation
|
| 462 |
+
if (window.location.hash) {
|
| 463 |
+
var target = document.querySelector(window.location.hash);
|
| 464 |
+
if (target) setTimeout(function() { target.scrollIntoView({block:'start'}); }, 100);
|
| 465 |
+
}
|
| 466 |
+
window.addEventListener('popstate', function() {
|
| 467 |
+
var h = window.location.hash;
|
| 468 |
+
if (h) { var t = document.querySelector(h); if (t) t.scrollIntoView({block:'start'}); }
|
| 469 |
+
else window.scrollTo({top:0});
|
| 470 |
+
});
|
| 471 |
+
})();
|
| 472 |
+
</script>
|
| 473 |
+
</body>
|
| 474 |
+
</html>"
|
| 475 |
+
`;
|
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, it, expect } from "vitest";
|
| 2 |
+
import { renderArticleHTML, type PublishMeta, type CitationData } from "../src/publisher/html-renderer.js";
|
| 3 |
+
import type { PublishCSS } from "../src/publisher/index.js";
|
| 4 |
+
|
| 5 |
+
const EMPTY_CSS: PublishCSS = {
|
| 6 |
+
variables: "",
|
| 7 |
+
reset: "",
|
| 8 |
+
base: "",
|
| 9 |
+
layout: "",
|
| 10 |
+
print: "",
|
| 11 |
+
editorTokens: "",
|
| 12 |
+
article: "",
|
| 13 |
+
components: "",
|
| 14 |
+
publisher: "",
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
const BASIC_META: PublishMeta = {
|
| 18 |
+
title: "Test Article",
|
| 19 |
+
description: "A test article",
|
| 20 |
+
authors: [{ name: "Alice", affiliationIndices: [1], affiliationNames: ["MIT"] }],
|
| 21 |
+
affiliations: [{ name: "MIT" }],
|
| 22 |
+
date: "2025-01-01",
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
function minimalDoc(content: any[]) {
|
| 26 |
+
return { type: "doc", content };
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
describe("renderArticleHTML", () => {
|
| 30 |
+
it("produces a complete HTML document", () => {
|
| 31 |
+
const json = minimalDoc([{ type: "paragraph", content: [{ type: "text", text: "Hello world" }] }]);
|
| 32 |
+
const html = renderArticleHTML(json, BASIC_META, EMPTY_CSS);
|
| 33 |
+
expect(html).toContain("<!DOCTYPE html>");
|
| 34 |
+
expect(html).toContain("<title>Test Article</title>");
|
| 35 |
+
expect(html).toContain("Hello world");
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
it("escapes HTML in meta fields", () => {
|
| 39 |
+
const meta: PublishMeta = { ...BASIC_META, title: 'Title with "quotes" & <tags>' };
|
| 40 |
+
const json = minimalDoc([{ type: "paragraph" }]);
|
| 41 |
+
const html = renderArticleHTML(json, meta, EMPTY_CSS);
|
| 42 |
+
expect(html).toContain("&");
|
| 43 |
+
expect(html).toContain("<tags>");
|
| 44 |
+
expect(html).not.toContain('<tags>');
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
it("renders PDF download link when pdfUrl is set", () => {
|
| 48 |
+
const meta: PublishMeta = { ...BASIC_META, pdfUrl: "/published/test/article.pdf" };
|
| 49 |
+
const json = minimalDoc([{ type: "paragraph" }]);
|
| 50 |
+
const html = renderArticleHTML(json, meta, EMPTY_CSS);
|
| 51 |
+
expect(html).toContain("Download PDF");
|
| 52 |
+
expect(html).toContain("/published/test/article.pdf");
|
| 53 |
+
});
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
describe("postProcess - accordion", () => {
|
| 57 |
+
it("transforms accordion div into details/summary", () => {
|
| 58 |
+
const json = minimalDoc([{
|
| 59 |
+
type: "accordion",
|
| 60 |
+
attrs: { title: "My Section", open: false },
|
| 61 |
+
content: [{ type: "paragraph", content: [{ type: "text", text: "Inner content" }] }],
|
| 62 |
+
}]);
|
| 63 |
+
const html = renderArticleHTML(json, BASIC_META, EMPTY_CSS);
|
| 64 |
+
expect(html).toContain("<details");
|
| 65 |
+
expect(html).toContain("<summary>");
|
| 66 |
+
expect(html).toContain("Inner content");
|
| 67 |
+
});
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
describe("postProcess - citations", () => {
|
| 71 |
+
it("replaces citation spans with anchor links", () => {
|
| 72 |
+
const json = minimalDoc([{
|
| 73 |
+
type: "paragraph",
|
| 74 |
+
content: [
|
| 75 |
+
{ type: "text", text: "See " },
|
| 76 |
+
{ type: "citation", attrs: { key: "smith2024", label: "Smith (2024)" } },
|
| 77 |
+
],
|
| 78 |
+
}]);
|
| 79 |
+
const citationData: CitationData = {
|
| 80 |
+
entries: [{ id: "smith2024", title: "Test Paper" }],
|
| 81 |
+
orderedKeys: ["smith2024"],
|
| 82 |
+
style: "apa",
|
| 83 |
+
};
|
| 84 |
+
const html = renderArticleHTML(json, BASIC_META, EMPTY_CSS, citationData);
|
| 85 |
+
expect(html).toContain('href="#ref-smith2024"');
|
| 86 |
+
expect(html).toContain("citation-inline");
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
+
it("uses numeric labels for IEEE style", () => {
|
| 90 |
+
const json = minimalDoc([{
|
| 91 |
+
type: "paragraph",
|
| 92 |
+
content: [
|
| 93 |
+
{ type: "citation", attrs: { key: "doe2023", label: "Doe (2023)" } },
|
| 94 |
+
],
|
| 95 |
+
}]);
|
| 96 |
+
const citationData: CitationData = {
|
| 97 |
+
entries: [{ id: "doe2023" }],
|
| 98 |
+
orderedKeys: ["doe2023"],
|
| 99 |
+
style: "ieee",
|
| 100 |
+
};
|
| 101 |
+
const html = renderArticleHTML(json, BASIC_META, EMPTY_CSS, citationData);
|
| 102 |
+
expect(html).toContain("[1]");
|
| 103 |
+
});
|
| 104 |
+
});
|
| 105 |
+
|
| 106 |
+
describe("postProcess - footnotes", () => {
|
| 107 |
+
it("collects footnotes and appends a footnotes section", () => {
|
| 108 |
+
const json = minimalDoc([{
|
| 109 |
+
type: "paragraph",
|
| 110 |
+
content: [
|
| 111 |
+
{ type: "text", text: "Text" },
|
| 112 |
+
{ type: "footnote", attrs: { content: "This is a footnote" } },
|
| 113 |
+
],
|
| 114 |
+
}]);
|
| 115 |
+
const html = renderArticleHTML(json, BASIC_META, EMPTY_CSS);
|
| 116 |
+
expect(html).toContain('class="footnote-ref"');
|
| 117 |
+
expect(html).toContain('id="fn-1"');
|
| 118 |
+
expect(html).toContain("This is a footnote");
|
| 119 |
+
expect(html).toContain('class="footnotes"');
|
| 120 |
+
});
|
| 121 |
+
|
| 122 |
+
it("numbers multiple footnotes sequentially", () => {
|
| 123 |
+
const json = minimalDoc([{
|
| 124 |
+
type: "paragraph",
|
| 125 |
+
content: [
|
| 126 |
+
{ type: "footnote", attrs: { content: "First note" } },
|
| 127 |
+
{ type: "text", text: " and " },
|
| 128 |
+
{ type: "footnote", attrs: { content: "Second note" } },
|
| 129 |
+
],
|
| 130 |
+
}]);
|
| 131 |
+
const html = renderArticleHTML(json, BASIC_META, EMPTY_CSS);
|
| 132 |
+
expect(html).toContain('id="fn-1"');
|
| 133 |
+
expect(html).toContain('id="fn-2"');
|
| 134 |
+
expect(html).toContain("First note");
|
| 135 |
+
expect(html).toContain("Second note");
|
| 136 |
+
});
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
describe("postProcess - mermaid", () => {
|
| 140 |
+
it("transforms mermaid div into pre.mermaid", () => {
|
| 141 |
+
const json = minimalDoc([{
|
| 142 |
+
type: "mermaid",
|
| 143 |
+
attrs: { code: "graph TD\n A --> B" },
|
| 144 |
+
}]);
|
| 145 |
+
const html = renderArticleHTML(json, BASIC_META, EMPTY_CSS);
|
| 146 |
+
expect(html).toContain('class="mermaid"');
|
| 147 |
+
expect(html).toContain("graph TD");
|
| 148 |
+
});
|
| 149 |
+
});
|
| 150 |
+
|
| 151 |
+
describe("postProcess - htmlEmbed", () => {
|
| 152 |
+
it("transforms htmlEmbed div into iframe", () => {
|
| 153 |
+
const json = minimalDoc([{
|
| 154 |
+
type: "htmlEmbed",
|
| 155 |
+
attrs: { src: "d3-chart.html", title: "Chart", desc: "" },
|
| 156 |
+
}]);
|
| 157 |
+
const html = renderArticleHTML(json, BASIC_META, EMPTY_CSS);
|
| 158 |
+
expect(html).toContain("html-embed-container");
|
| 159 |
+
expect(html).toContain('data-embed-src="d3-chart.html"');
|
| 160 |
+
expect(html).toContain("<iframe");
|
| 161 |
+
});
|
| 162 |
+
});
|
| 163 |
+
|
| 164 |
+
describe("postProcess - bibliography", () => {
|
| 165 |
+
it("injects bibliography HTML into the placeholder", () => {
|
| 166 |
+
const json = minimalDoc([
|
| 167 |
+
{ type: "paragraph", content: [{ type: "citation", attrs: { key: "test2024", label: "[1]" } }] },
|
| 168 |
+
{ type: "bibliography", attrs: { renderedHtml: "" } },
|
| 169 |
+
]);
|
| 170 |
+
const citationData: CitationData = {
|
| 171 |
+
entries: [{ id: "test2024" }],
|
| 172 |
+
orderedKeys: ["test2024"],
|
| 173 |
+
style: "apa",
|
| 174 |
+
};
|
| 175 |
+
const biblioHtml = '<div class="csl-entry">Test entry</div>';
|
| 176 |
+
const html = renderArticleHTML(json, BASIC_META, EMPTY_CSS, citationData, biblioHtml);
|
| 177 |
+
expect(html).toContain("bibliography-content");
|
| 178 |
+
expect(html).toContain('id="ref-test2024"');
|
| 179 |
+
expect(html).toContain("Test entry");
|
| 180 |
+
});
|
| 181 |
+
});
|
| 182 |
+
|
| 183 |
+
describe("snapshot - full render", () => {
|
| 184 |
+
it("matches snapshot for a typical article", () => {
|
| 185 |
+
const json = minimalDoc([
|
| 186 |
+
{ type: "heading", attrs: { level: 2 }, content: [{ type: "text", text: "Introduction" }] },
|
| 187 |
+
{ type: "paragraph", content: [
|
| 188 |
+
{ type: "text", text: "This is a test article with a " },
|
| 189 |
+
{ type: "citation", attrs: { key: "ref1", label: "Ref (2024)" } },
|
| 190 |
+
{ type: "text", text: " citation." },
|
| 191 |
+
] },
|
| 192 |
+
{ type: "paragraph", content: [
|
| 193 |
+
{ type: "text", text: "A footnote here" },
|
| 194 |
+
{ type: "footnote", attrs: { content: "Important detail" } },
|
| 195 |
+
] },
|
| 196 |
+
{ type: "bibliography", attrs: { renderedHtml: "" } },
|
| 197 |
+
]);
|
| 198 |
+
|
| 199 |
+
const citationData: CitationData = {
|
| 200 |
+
entries: [{ id: "ref1", title: "Reference One" }],
|
| 201 |
+
orderedKeys: ["ref1"],
|
| 202 |
+
style: "apa",
|
| 203 |
+
};
|
| 204 |
+
const biblioHtml = '<div class="csl-entry">Reference One. 2024.</div>';
|
| 205 |
+
|
| 206 |
+
const html = renderArticleHTML(json, BASIC_META, EMPTY_CSS, citationData, biblioHtml);
|
| 207 |
+
expect(html).toMatchSnapshot();
|
| 208 |
+
});
|
| 209 |
+
});
|
|
@@ -39,6 +39,7 @@ const EMPTY_CSS = {
|
|
| 39 |
editorTokens: "",
|
| 40 |
article: "",
|
| 41 |
components: "",
|
|
|
|
| 42 |
};
|
| 43 |
|
| 44 |
// ── 1.1 Y.Doc Extraction ──────────────────────────────────────────
|
|
|
|
| 39 |
editorTokens: "",
|
| 40 |
article: "",
|
| 41 |
components: "",
|
| 42 |
+
publisher: "",
|
| 43 |
};
|
| 44 |
|
| 45 |
// ── 1.1 Y.Doc Extraction ──────────────────────────────────────────
|
|
@@ -15,6 +15,7 @@ const EMPTY_CSS = {
|
|
| 15 |
editorTokens: "",
|
| 16 |
article: "",
|
| 17 |
components: "",
|
|
|
|
| 18 |
};
|
| 19 |
|
| 20 |
const BASE_META: PublishMeta = {
|
|
|
|
| 15 |
editorTokens: "",
|
| 16 |
article: "",
|
| 17 |
components: "",
|
| 18 |
+
publisher: "",
|
| 19 |
};
|
| 20 |
|
| 21 |
const BASE_META: PublishMeta = {
|
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Architecture
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The collab-editor is a collaborative article editor built for Hugging Face Spaces. It runs as a single Docker container serving both the backend (Express + Hocuspocus) and the frontend (React + TipTap).
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
┌─────────────────────────────────────────────────────────┐
|
| 9 |
+
│ Docker Container (port 8080) │
|
| 10 |
+
│ │
|
| 11 |
+
│ ┌──────────────────┐ ┌────────────────────────────┐ │
|
| 12 |
+
│ │ Express Server │ │ Hocuspocus (Y.js collab) │ │
|
| 13 |
+
│ │ │ │ │ │
|
| 14 |
+
│ │ /api/* │ │ /collab (WebSocket) │ │
|
| 15 |
+
│ │ /published/* │ │ │ │
|
| 16 |
+
│ │ /editor │ │ │ │
|
| 17 |
+
│ │ / (published) │ │ │ │
|
| 18 |
+
│ └──────────────────┘ └────────────────────────────┘ │
|
| 19 |
+
│ │
|
| 20 |
+
│ ┌──────────────────┐ ┌────────────────────────────┐ │
|
| 21 |
+
│ │ Publisher │ │ Frontend (static) │ │
|
| 22 |
+
│ │ (HTML renderer) │ │ /editor -> SPA │ │
|
| 23 |
+
│ │ (PDF generator) │ │ React + TipTap │ │
|
| 24 |
+
│ └──────────────────┘ └────────────────────────────┘ │
|
| 25 |
+
└─────────────────────────────────────────────────────────┘
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## Key directories
|
| 29 |
+
|
| 30 |
+
| Path | Description |
|
| 31 |
+
|------|-------------|
|
| 32 |
+
| `backend/src/server.ts` | Express server, routes, WebSocket setup |
|
| 33 |
+
| `backend/src/publisher/` | HTML rendering, PDF generation, bibliography formatting |
|
| 34 |
+
| `backend/src/publisher/html-renderer.ts` | Converts TipTap JSON to static HTML page |
|
| 35 |
+
| `backend/src/publisher/extensions.ts` | Server-side TipTap extensions (mirrors frontend) |
|
| 36 |
+
| `backend/src/shared/component-defs.ts` | Shared component definitions (single source of truth) |
|
| 37 |
+
| `frontend/src/editor/` | TipTap editor, toolbars, components |
|
| 38 |
+
| `frontend/src/editor/components/registry.ts` | Component registry (imports from shared defs) |
|
| 39 |
+
| `frontend/src/styles/` | All CSS files |
|
| 40 |
+
| `frontend/src/styles/_publisher.css` | Publisher-only CSS (injected into static HTML) |
|
| 41 |
+
| `frontend/src/styles/_ui.css` | Editor chrome CSS (buttons, dialogs, drawers) |
|
| 42 |
+
|
| 43 |
+
## Styling architecture
|
| 44 |
+
|
| 45 |
+
### CSS layers
|
| 46 |
+
|
| 47 |
+
The project uses three CSS layers:
|
| 48 |
+
|
| 49 |
+
1. **Template CSS** (`_variables.css`, `_base.css`, `_layout.css`, `article.css`, `components/`)
|
| 50 |
+
- Defines the article's visual identity
|
| 51 |
+
- Uses CSS custom properties (`--text-color`, `--surface-bg`, `--primary-color`, etc.)
|
| 52 |
+
- Shared between the editor preview and the published output
|
| 53 |
+
|
| 54 |
+
2. **Editor chrome CSS** (`_ui.css`)
|
| 55 |
+
- Styles the editor UI: toolbars, sidebars, dialogs, forms
|
| 56 |
+
- Uses `--ed-*` custom properties for the dark editor theme
|
| 57 |
+
- Only loaded in the editor, never in published output
|
| 58 |
+
|
| 59 |
+
3. **Publisher CSS** (`_publisher.css`)
|
| 60 |
+
- Styles specific to the published static HTML page
|
| 61 |
+
- Footer layout, bibliography, footnotes, citation links, theme toggle
|
| 62 |
+
- Only injected by the HTML renderer, never loaded in the editor
|
| 63 |
+
|
| 64 |
+
### No CSS-in-JS
|
| 65 |
+
|
| 66 |
+
The project does not use MUI, Emotion, or any CSS-in-JS library. All styling is done via:
|
| 67 |
+
- CSS custom properties for theming
|
| 68 |
+
- Vanilla CSS files with BEM-like class naming
|
| 69 |
+
- `Floating UI` for tooltip positioning (lightweight, no CSS-in-JS)
|
| 70 |
+
|
| 71 |
+
### Color theming
|
| 72 |
+
|
| 73 |
+
- The article area uses `data-theme="light"` / `data-theme="dark"` with CSS variable overrides
|
| 74 |
+
- The editor chrome is always dark, using `--ed-*` tokens
|
| 75 |
+
- The primary accent color is controlled via `--primary-color` (synced via Yjs settings)
|
| 76 |
+
|
| 77 |
+
## HF Spaces constraints
|
| 78 |
+
|
| 79 |
+
### Iframe embedding
|
| 80 |
+
|
| 81 |
+
When deployed as a HF Space, the app runs inside an iframe. This affects:
|
| 82 |
+
|
| 83 |
+
- **Viewport width**: the iframe is ~968px wide, not the full browser width
|
| 84 |
+
- **No `target="_top"` navigation**: links open within the iframe unless using `target="_blank"`
|
| 85 |
+
- **OAuth flow**: the OAuth callback URL must match the Space URL
|
| 86 |
+
- **CSP restrictions**: sandboxed iframes may restrict certain APIs
|
| 87 |
+
|
| 88 |
+
### Two-page architecture
|
| 89 |
+
|
| 90 |
+
| URL | What it serves |
|
| 91 |
+
|-----|----------------|
|
| 92 |
+
| `/` | Published article (static HTML) or login prompt |
|
| 93 |
+
| `/editor` | The SPA editor (requires authentication) |
|
| 94 |
+
|
| 95 |
+
This means:
|
| 96 |
+
- The published article is a completely standalone HTML file
|
| 97 |
+
- It does NOT load React or any JS framework
|
| 98 |
+
- The editor is a separate React SPA at `/editor`
|
| 99 |
+
|
| 100 |
+
### CSS cascade
|
| 101 |
+
|
| 102 |
+
Because the published HTML is self-contained (all CSS inlined in `<style>`), there are no CSS conflicts with the HF Spaces iframe CSS. The editor uses Vite's CSS pipeline.
|
| 103 |
+
|
| 104 |
+
## Shared component registry
|
| 105 |
+
|
| 106 |
+
Component definitions (name, kind, fields, defaults) are defined once in `backend/src/shared/component-defs.ts`. This file is the single source of truth.
|
| 107 |
+
|
| 108 |
+
- **Backend** (`extensions.ts`): imports `SHARED_COMPONENT_DEFS` to generate TipTap server extensions for `generateHTML()`
|
| 109 |
+
- **Frontend** (`registry.ts`): imports `SHARED_COMPONENT_DEFS` via Vite alias `#shared` and decorates each entry with UI metadata (icon, label, description, placeholders)
|
| 110 |
+
|
| 111 |
+
Adding a new component:
|
| 112 |
+
1. Add the entry to `shared/component-defs.ts`
|
| 113 |
+
2. Add UI metadata to `frontend/src/editor/components/registry.ts` in `UI_META`
|
| 114 |
+
3. Add CSS for the published view to `frontend/src/styles/_publisher.css`
|
| 115 |
+
|
| 116 |
+
## Publisher pipeline
|
| 117 |
+
|
| 118 |
+
```
|
| 119 |
+
Y.Doc -> TiptapTransformer -> JSON -> generateHTML() -> postProcess() -> full HTML page
|
| 120 |
+
|
|
| 121 |
+
v
|
| 122 |
+
PDF (Playwright)
|
| 123 |
+
|
|
| 124 |
+
v
|
| 125 |
+
Upload to HF dataset
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
### Post-processing (linkedom)
|
| 129 |
+
|
| 130 |
+
The `postProcess()` function uses `linkedom` for DOM manipulation instead of regex:
|
| 131 |
+
- Accordion `<div>` -> `<details>/<summary>`
|
| 132 |
+
- Citation `<span>` -> `<a>` links with bibliography anchors
|
| 133 |
+
- Bibliography placeholder -> formatted HTML with entry IDs
|
| 134 |
+
- Mermaid `<div>` -> `<pre class="mermaid">`
|
| 135 |
+
- HtmlEmbed `<div>` -> `<iframe>`
|
| 136 |
+
- Footnotes -> superscript links + appended section
|
| 137 |
+
|
| 138 |
+
### Preview endpoint
|
| 139 |
+
|
| 140 |
+
`GET /api/preview/:docName` renders the HTML without saving or uploading. Useful for testing the publisher pipeline.
|
| 141 |
+
|
| 142 |
+
## Testing
|
| 143 |
+
|
| 144 |
+
Tests use Vitest. Run with `npm test` from `backend/`.
|
| 145 |
+
|
| 146 |
+
| Test file | What it covers |
|
| 147 |
+
|-----------|----------------|
|
| 148 |
+
| `tests/publisher.test.ts` | Y.Doc extraction, HTML generation, post-processing, idempotency |
|
| 149 |
+
| `tests/html-renderer-snapshot.test.ts` | Snapshot tests for each postProcess transformation |
|
| 150 |
+
| `tests/security.test.ts` | XSS prevention in published HTML |
|
| 151 |
+
| `tests/css-resolution.test.ts` | @custom-media resolution |
|
| 152 |
+
| `tests/utils.test.ts` | Path sanitization utilities |
|
| 153 |
+
| `tests/auth.test.ts` | Token extraction, OAuth configuration |
|
| 154 |
+
| `tests/hf-storage.test.ts` | HF dataset storage configuration |
|
|
@@ -9,12 +9,8 @@
|
|
| 9 |
"version": "0.1.0",
|
| 10 |
"dependencies": {
|
| 11 |
"@ai-sdk/react": "^3.0.160",
|
| 12 |
-
"@emotion/react": "^11.14.0",
|
| 13 |
-
"@emotion/styled": "^11.14.1",
|
| 14 |
"@floating-ui/dom": "^1.7.6",
|
| 15 |
"@hocuspocus/provider": "^3.4.4",
|
| 16 |
-
"@mui/icons-material": "^9.0.0",
|
| 17 |
-
"@mui/material": "^9.0.0",
|
| 18 |
"@tiptap/core": "^3.22.3",
|
| 19 |
"@tiptap/extension-code-block-lowlight": "^3.22.3",
|
| 20 |
"@tiptap/extension-collaboration": "^3.22.3",
|
|
@@ -36,6 +32,7 @@
|
|
| 36 |
"ai": "^6.0.158",
|
| 37 |
"katex": "^0.16.45",
|
| 38 |
"lowlight": "^3.2.0",
|
|
|
|
| 39 |
"mermaid": "^11.14.0",
|
| 40 |
"react": "^18.3.0",
|
| 41 |
"react-dom": "^18.3.0",
|
|
@@ -132,6 +129,7 @@
|
|
| 132 |
"version": "7.29.0",
|
| 133 |
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
| 134 |
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
|
|
|
| 135 |
"license": "MIT",
|
| 136 |
"dependencies": {
|
| 137 |
"@babel/helper-validator-identifier": "^7.28.5",
|
|
@@ -195,6 +193,7 @@
|
|
| 195 |
"version": "7.29.1",
|
| 196 |
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
|
| 197 |
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
|
|
|
| 198 |
"license": "MIT",
|
| 199 |
"dependencies": {
|
| 200 |
"@babel/parser": "^7.29.0",
|
|
@@ -228,6 +227,7 @@
|
|
| 228 |
"version": "7.28.0",
|
| 229 |
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
| 230 |
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
|
|
|
| 231 |
"license": "MIT",
|
| 232 |
"engines": {
|
| 233 |
"node": ">=6.9.0"
|
|
@@ -237,6 +237,7 @@
|
|
| 237 |
"version": "7.28.6",
|
| 238 |
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
|
| 239 |
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
|
|
|
|
| 240 |
"license": "MIT",
|
| 241 |
"dependencies": {
|
| 242 |
"@babel/traverse": "^7.28.6",
|
|
@@ -278,6 +279,7 @@
|
|
| 278 |
"version": "7.27.1",
|
| 279 |
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
| 280 |
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
|
|
|
| 281 |
"license": "MIT",
|
| 282 |
"engines": {
|
| 283 |
"node": ">=6.9.0"
|
|
@@ -287,6 +289,7 @@
|
|
| 287 |
"version": "7.28.5",
|
| 288 |
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
| 289 |
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
|
|
|
| 290 |
"license": "MIT",
|
| 291 |
"engines": {
|
| 292 |
"node": ">=6.9.0"
|
|
@@ -320,6 +323,7 @@
|
|
| 320 |
"version": "7.29.2",
|
| 321 |
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
|
| 322 |
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
|
|
|
|
| 323 |
"license": "MIT",
|
| 324 |
"dependencies": {
|
| 325 |
"@babel/types": "^7.29.0"
|
|
@@ -363,19 +367,11 @@
|
|
| 363 |
"@babel/core": "^7.0.0-0"
|
| 364 |
}
|
| 365 |
},
|
| 366 |
-
"node_modules/@babel/runtime": {
|
| 367 |
-
"version": "7.29.2",
|
| 368 |
-
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
| 369 |
-
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
| 370 |
-
"license": "MIT",
|
| 371 |
-
"engines": {
|
| 372 |
-
"node": ">=6.9.0"
|
| 373 |
-
}
|
| 374 |
-
},
|
| 375 |
"node_modules/@babel/template": {
|
| 376 |
"version": "7.28.6",
|
| 377 |
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
| 378 |
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
|
|
|
| 379 |
"license": "MIT",
|
| 380 |
"dependencies": {
|
| 381 |
"@babel/code-frame": "^7.28.6",
|
|
@@ -390,6 +386,7 @@
|
|
| 390 |
"version": "7.29.0",
|
| 391 |
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
|
| 392 |
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
|
|
|
| 393 |
"license": "MIT",
|
| 394 |
"dependencies": {
|
| 395 |
"@babel/code-frame": "^7.29.0",
|
|
@@ -408,6 +405,7 @@
|
|
| 408 |
"version": "7.29.0",
|
| 409 |
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
| 410 |
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
|
|
|
| 411 |
"license": "MIT",
|
| 412 |
"dependencies": {
|
| 413 |
"@babel/helper-string-parser": "^7.27.1",
|
|
@@ -553,154 +551,6 @@
|
|
| 553 |
"@csstools/css-tokenizer": "^4.0.0"
|
| 554 |
}
|
| 555 |
},
|
| 556 |
-
"node_modules/@emotion/babel-plugin": {
|
| 557 |
-
"version": "11.13.5",
|
| 558 |
-
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
| 559 |
-
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
|
| 560 |
-
"license": "MIT",
|
| 561 |
-
"dependencies": {
|
| 562 |
-
"@babel/helper-module-imports": "^7.16.7",
|
| 563 |
-
"@babel/runtime": "^7.18.3",
|
| 564 |
-
"@emotion/hash": "^0.9.2",
|
| 565 |
-
"@emotion/memoize": "^0.9.0",
|
| 566 |
-
"@emotion/serialize": "^1.3.3",
|
| 567 |
-
"babel-plugin-macros": "^3.1.0",
|
| 568 |
-
"convert-source-map": "^1.5.0",
|
| 569 |
-
"escape-string-regexp": "^4.0.0",
|
| 570 |
-
"find-root": "^1.1.0",
|
| 571 |
-
"source-map": "^0.5.7",
|
| 572 |
-
"stylis": "4.2.0"
|
| 573 |
-
}
|
| 574 |
-
},
|
| 575 |
-
"node_modules/@emotion/cache": {
|
| 576 |
-
"version": "11.14.0",
|
| 577 |
-
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
|
| 578 |
-
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
|
| 579 |
-
"license": "MIT",
|
| 580 |
-
"dependencies": {
|
| 581 |
-
"@emotion/memoize": "^0.9.0",
|
| 582 |
-
"@emotion/sheet": "^1.4.0",
|
| 583 |
-
"@emotion/utils": "^1.4.2",
|
| 584 |
-
"@emotion/weak-memoize": "^0.4.0",
|
| 585 |
-
"stylis": "4.2.0"
|
| 586 |
-
}
|
| 587 |
-
},
|
| 588 |
-
"node_modules/@emotion/hash": {
|
| 589 |
-
"version": "0.9.2",
|
| 590 |
-
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
|
| 591 |
-
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
|
| 592 |
-
"license": "MIT"
|
| 593 |
-
},
|
| 594 |
-
"node_modules/@emotion/is-prop-valid": {
|
| 595 |
-
"version": "1.4.0",
|
| 596 |
-
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
|
| 597 |
-
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
|
| 598 |
-
"license": "MIT",
|
| 599 |
-
"dependencies": {
|
| 600 |
-
"@emotion/memoize": "^0.9.0"
|
| 601 |
-
}
|
| 602 |
-
},
|
| 603 |
-
"node_modules/@emotion/memoize": {
|
| 604 |
-
"version": "0.9.0",
|
| 605 |
-
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
|
| 606 |
-
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
|
| 607 |
-
"license": "MIT"
|
| 608 |
-
},
|
| 609 |
-
"node_modules/@emotion/react": {
|
| 610 |
-
"version": "11.14.0",
|
| 611 |
-
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
| 612 |
-
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
| 613 |
-
"license": "MIT",
|
| 614 |
-
"peer": true,
|
| 615 |
-
"dependencies": {
|
| 616 |
-
"@babel/runtime": "^7.18.3",
|
| 617 |
-
"@emotion/babel-plugin": "^11.13.5",
|
| 618 |
-
"@emotion/cache": "^11.14.0",
|
| 619 |
-
"@emotion/serialize": "^1.3.3",
|
| 620 |
-
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
|
| 621 |
-
"@emotion/utils": "^1.4.2",
|
| 622 |
-
"@emotion/weak-memoize": "^0.4.0",
|
| 623 |
-
"hoist-non-react-statics": "^3.3.1"
|
| 624 |
-
},
|
| 625 |
-
"peerDependencies": {
|
| 626 |
-
"react": ">=16.8.0"
|
| 627 |
-
},
|
| 628 |
-
"peerDependenciesMeta": {
|
| 629 |
-
"@types/react": {
|
| 630 |
-
"optional": true
|
| 631 |
-
}
|
| 632 |
-
}
|
| 633 |
-
},
|
| 634 |
-
"node_modules/@emotion/serialize": {
|
| 635 |
-
"version": "1.3.3",
|
| 636 |
-
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
|
| 637 |
-
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
|
| 638 |
-
"license": "MIT",
|
| 639 |
-
"dependencies": {
|
| 640 |
-
"@emotion/hash": "^0.9.2",
|
| 641 |
-
"@emotion/memoize": "^0.9.0",
|
| 642 |
-
"@emotion/unitless": "^0.10.0",
|
| 643 |
-
"@emotion/utils": "^1.4.2",
|
| 644 |
-
"csstype": "^3.0.2"
|
| 645 |
-
}
|
| 646 |
-
},
|
| 647 |
-
"node_modules/@emotion/sheet": {
|
| 648 |
-
"version": "1.4.0",
|
| 649 |
-
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
|
| 650 |
-
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
|
| 651 |
-
"license": "MIT"
|
| 652 |
-
},
|
| 653 |
-
"node_modules/@emotion/styled": {
|
| 654 |
-
"version": "11.14.1",
|
| 655 |
-
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
|
| 656 |
-
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
|
| 657 |
-
"license": "MIT",
|
| 658 |
-
"peer": true,
|
| 659 |
-
"dependencies": {
|
| 660 |
-
"@babel/runtime": "^7.18.3",
|
| 661 |
-
"@emotion/babel-plugin": "^11.13.5",
|
| 662 |
-
"@emotion/is-prop-valid": "^1.3.0",
|
| 663 |
-
"@emotion/serialize": "^1.3.3",
|
| 664 |
-
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
|
| 665 |
-
"@emotion/utils": "^1.4.2"
|
| 666 |
-
},
|
| 667 |
-
"peerDependencies": {
|
| 668 |
-
"@emotion/react": "^11.0.0-rc.0",
|
| 669 |
-
"react": ">=16.8.0"
|
| 670 |
-
},
|
| 671 |
-
"peerDependenciesMeta": {
|
| 672 |
-
"@types/react": {
|
| 673 |
-
"optional": true
|
| 674 |
-
}
|
| 675 |
-
}
|
| 676 |
-
},
|
| 677 |
-
"node_modules/@emotion/unitless": {
|
| 678 |
-
"version": "0.10.0",
|
| 679 |
-
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
|
| 680 |
-
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
|
| 681 |
-
"license": "MIT"
|
| 682 |
-
},
|
| 683 |
-
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
|
| 684 |
-
"version": "1.2.0",
|
| 685 |
-
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
|
| 686 |
-
"integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
|
| 687 |
-
"license": "MIT",
|
| 688 |
-
"peerDependencies": {
|
| 689 |
-
"react": ">=16.8.0"
|
| 690 |
-
}
|
| 691 |
-
},
|
| 692 |
-
"node_modules/@emotion/utils": {
|
| 693 |
-
"version": "1.4.2",
|
| 694 |
-
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
|
| 695 |
-
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
|
| 696 |
-
"license": "MIT"
|
| 697 |
-
},
|
| 698 |
-
"node_modules/@emotion/weak-memoize": {
|
| 699 |
-
"version": "0.4.0",
|
| 700 |
-
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
|
| 701 |
-
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
|
| 702 |
-
"license": "MIT"
|
| 703 |
-
},
|
| 704 |
"node_modules/@esbuild/aix-ppc64": {
|
| 705 |
"version": "0.25.12",
|
| 706 |
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
|
@@ -1236,6 +1086,7 @@
|
|
| 1236 |
"version": "0.3.13",
|
| 1237 |
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
| 1238 |
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
|
|
|
| 1239 |
"license": "MIT",
|
| 1240 |
"dependencies": {
|
| 1241 |
"@jridgewell/sourcemap-codec": "^1.5.0",
|
|
@@ -1257,6 +1108,7 @@
|
|
| 1257 |
"version": "3.1.2",
|
| 1258 |
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
| 1259 |
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
|
|
|
| 1260 |
"license": "MIT",
|
| 1261 |
"engines": {
|
| 1262 |
"node": ">=6.0.0"
|
|
@@ -1266,12 +1118,14 @@
|
|
| 1266 |
"version": "1.5.5",
|
| 1267 |
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
| 1268 |
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
|
|
|
| 1269 |
"license": "MIT"
|
| 1270 |
},
|
| 1271 |
"node_modules/@jridgewell/trace-mapping": {
|
| 1272 |
"version": "0.3.31",
|
| 1273 |
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
| 1274 |
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
|
|
|
| 1275 |
"license": "MIT",
|
| 1276 |
"dependencies": {
|
| 1277 |
"@jridgewell/resolve-uri": "^3.1.0",
|
|
@@ -1293,240 +1147,6 @@
|
|
| 1293 |
"langium": "^4.0.0"
|
| 1294 |
}
|
| 1295 |
},
|
| 1296 |
-
"node_modules/@mui/core-downloads-tracker": {
|
| 1297 |
-
"version": "9.0.0",
|
| 1298 |
-
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.0.0.tgz",
|
| 1299 |
-
"integrity": "sha512-uwQNGkhv0lf7ufxw6QXev77BW6pWbW+7uxYjU5+rfp4lBkFtMEgJCsarTM3Tn+i0lGx6+Ol2u88JdGXr0GDskA==",
|
| 1300 |
-
"license": "MIT",
|
| 1301 |
-
"funding": {
|
| 1302 |
-
"type": "opencollective",
|
| 1303 |
-
"url": "https://opencollective.com/mui-org"
|
| 1304 |
-
}
|
| 1305 |
-
},
|
| 1306 |
-
"node_modules/@mui/icons-material": {
|
| 1307 |
-
"version": "9.0.0",
|
| 1308 |
-
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.0.0.tgz",
|
| 1309 |
-
"integrity": "sha512-oDwyvI6LgjWRC9MBcSGvLkPud9S9ELgSBQFYxa1rYcZn6Br55dn22SyvsPDMsn0G8OndFk53iMT45W5mNqrogw==",
|
| 1310 |
-
"license": "MIT",
|
| 1311 |
-
"dependencies": {
|
| 1312 |
-
"@babel/runtime": "^7.29.2"
|
| 1313 |
-
},
|
| 1314 |
-
"engines": {
|
| 1315 |
-
"node": ">=14.0.0"
|
| 1316 |
-
},
|
| 1317 |
-
"funding": {
|
| 1318 |
-
"type": "opencollective",
|
| 1319 |
-
"url": "https://opencollective.com/mui-org"
|
| 1320 |
-
},
|
| 1321 |
-
"peerDependencies": {
|
| 1322 |
-
"@mui/material": "^9.0.0",
|
| 1323 |
-
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
| 1324 |
-
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 1325 |
-
},
|
| 1326 |
-
"peerDependenciesMeta": {
|
| 1327 |
-
"@types/react": {
|
| 1328 |
-
"optional": true
|
| 1329 |
-
}
|
| 1330 |
-
}
|
| 1331 |
-
},
|
| 1332 |
-
"node_modules/@mui/material": {
|
| 1333 |
-
"version": "9.0.0",
|
| 1334 |
-
"resolved": "https://registry.npmjs.org/@mui/material/-/material-9.0.0.tgz",
|
| 1335 |
-
"integrity": "sha512-+VP/oQCDhDR87NQQgXnNBG8dwy6GNuQLnenS1pZvkbn2dKFSxRSRMybTpH9xUxXP+316mlYDy5CSbYtusnCWtw==",
|
| 1336 |
-
"license": "MIT",
|
| 1337 |
-
"peer": true,
|
| 1338 |
-
"dependencies": {
|
| 1339 |
-
"@babel/runtime": "^7.29.2",
|
| 1340 |
-
"@mui/core-downloads-tracker": "^9.0.0",
|
| 1341 |
-
"@mui/system": "^9.0.0",
|
| 1342 |
-
"@mui/types": "^9.0.0",
|
| 1343 |
-
"@mui/utils": "^9.0.0",
|
| 1344 |
-
"@popperjs/core": "^2.11.8",
|
| 1345 |
-
"@types/react-transition-group": "^4.4.12",
|
| 1346 |
-
"clsx": "^2.1.1",
|
| 1347 |
-
"csstype": "^3.2.3",
|
| 1348 |
-
"prop-types": "^15.8.1",
|
| 1349 |
-
"react-is": "^19.2.4",
|
| 1350 |
-
"react-transition-group": "^4.4.5"
|
| 1351 |
-
},
|
| 1352 |
-
"engines": {
|
| 1353 |
-
"node": ">=14.0.0"
|
| 1354 |
-
},
|
| 1355 |
-
"funding": {
|
| 1356 |
-
"type": "opencollective",
|
| 1357 |
-
"url": "https://opencollective.com/mui-org"
|
| 1358 |
-
},
|
| 1359 |
-
"peerDependencies": {
|
| 1360 |
-
"@emotion/react": "^11.5.0",
|
| 1361 |
-
"@emotion/styled": "^11.3.0",
|
| 1362 |
-
"@mui/material-pigment-css": "^9.0.0",
|
| 1363 |
-
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
| 1364 |
-
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
| 1365 |
-
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 1366 |
-
},
|
| 1367 |
-
"peerDependenciesMeta": {
|
| 1368 |
-
"@emotion/react": {
|
| 1369 |
-
"optional": true
|
| 1370 |
-
},
|
| 1371 |
-
"@emotion/styled": {
|
| 1372 |
-
"optional": true
|
| 1373 |
-
},
|
| 1374 |
-
"@mui/material-pigment-css": {
|
| 1375 |
-
"optional": true
|
| 1376 |
-
},
|
| 1377 |
-
"@types/react": {
|
| 1378 |
-
"optional": true
|
| 1379 |
-
}
|
| 1380 |
-
}
|
| 1381 |
-
},
|
| 1382 |
-
"node_modules/@mui/private-theming": {
|
| 1383 |
-
"version": "9.0.0",
|
| 1384 |
-
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.0.0.tgz",
|
| 1385 |
-
"integrity": "sha512-JtuZoaiCqwD6vjgYu6Xp3T7DZkrxJlgtDz5yESzhI34fEX5hHMh2VJUbuL9UOg8xrfIFMrq6dcYoH/7Zi4G0RA==",
|
| 1386 |
-
"license": "MIT",
|
| 1387 |
-
"dependencies": {
|
| 1388 |
-
"@babel/runtime": "^7.29.2",
|
| 1389 |
-
"@mui/utils": "^9.0.0",
|
| 1390 |
-
"prop-types": "^15.8.1"
|
| 1391 |
-
},
|
| 1392 |
-
"engines": {
|
| 1393 |
-
"node": ">=14.0.0"
|
| 1394 |
-
},
|
| 1395 |
-
"funding": {
|
| 1396 |
-
"type": "opencollective",
|
| 1397 |
-
"url": "https://opencollective.com/mui-org"
|
| 1398 |
-
},
|
| 1399 |
-
"peerDependencies": {
|
| 1400 |
-
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
| 1401 |
-
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 1402 |
-
},
|
| 1403 |
-
"peerDependenciesMeta": {
|
| 1404 |
-
"@types/react": {
|
| 1405 |
-
"optional": true
|
| 1406 |
-
}
|
| 1407 |
-
}
|
| 1408 |
-
},
|
| 1409 |
-
"node_modules/@mui/styled-engine": {
|
| 1410 |
-
"version": "9.0.0",
|
| 1411 |
-
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.0.0.tgz",
|
| 1412 |
-
"integrity": "sha512-9RLGdX4Jg0aQPRuvqh/OLzYSPlgd5zyEw5/1HIRfdavSiOd03WtUaGZH9/w1RoTYuRKwpgy0hpIFaMHIqPVIWg==",
|
| 1413 |
-
"license": "MIT",
|
| 1414 |
-
"dependencies": {
|
| 1415 |
-
"@babel/runtime": "^7.29.2",
|
| 1416 |
-
"@emotion/cache": "^11.14.0",
|
| 1417 |
-
"@emotion/serialize": "^1.3.3",
|
| 1418 |
-
"@emotion/sheet": "^1.4.0",
|
| 1419 |
-
"csstype": "^3.2.3",
|
| 1420 |
-
"prop-types": "^15.8.1"
|
| 1421 |
-
},
|
| 1422 |
-
"engines": {
|
| 1423 |
-
"node": ">=14.0.0"
|
| 1424 |
-
},
|
| 1425 |
-
"funding": {
|
| 1426 |
-
"type": "opencollective",
|
| 1427 |
-
"url": "https://opencollective.com/mui-org"
|
| 1428 |
-
},
|
| 1429 |
-
"peerDependencies": {
|
| 1430 |
-
"@emotion/react": "^11.4.1",
|
| 1431 |
-
"@emotion/styled": "^11.3.0",
|
| 1432 |
-
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 1433 |
-
},
|
| 1434 |
-
"peerDependenciesMeta": {
|
| 1435 |
-
"@emotion/react": {
|
| 1436 |
-
"optional": true
|
| 1437 |
-
},
|
| 1438 |
-
"@emotion/styled": {
|
| 1439 |
-
"optional": true
|
| 1440 |
-
}
|
| 1441 |
-
}
|
| 1442 |
-
},
|
| 1443 |
-
"node_modules/@mui/system": {
|
| 1444 |
-
"version": "9.0.0",
|
| 1445 |
-
"resolved": "https://registry.npmjs.org/@mui/system/-/system-9.0.0.tgz",
|
| 1446 |
-
"integrity": "sha512-YnC5Zg6j04IxiLc/boAKs0464jfZlLFVa7mf5E8lF0XOtZVUvG6R6gJK50lgUYdaaLdyLfxF6xR7LaPuEpeT/g==",
|
| 1447 |
-
"license": "MIT",
|
| 1448 |
-
"dependencies": {
|
| 1449 |
-
"@babel/runtime": "^7.29.2",
|
| 1450 |
-
"@mui/private-theming": "^9.0.0",
|
| 1451 |
-
"@mui/styled-engine": "^9.0.0",
|
| 1452 |
-
"@mui/types": "^9.0.0",
|
| 1453 |
-
"@mui/utils": "^9.0.0",
|
| 1454 |
-
"clsx": "^2.1.1",
|
| 1455 |
-
"csstype": "^3.2.3",
|
| 1456 |
-
"prop-types": "^15.8.1"
|
| 1457 |
-
},
|
| 1458 |
-
"engines": {
|
| 1459 |
-
"node": ">=14.0.0"
|
| 1460 |
-
},
|
| 1461 |
-
"funding": {
|
| 1462 |
-
"type": "opencollective",
|
| 1463 |
-
"url": "https://opencollective.com/mui-org"
|
| 1464 |
-
},
|
| 1465 |
-
"peerDependencies": {
|
| 1466 |
-
"@emotion/react": "^11.5.0",
|
| 1467 |
-
"@emotion/styled": "^11.3.0",
|
| 1468 |
-
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
| 1469 |
-
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 1470 |
-
},
|
| 1471 |
-
"peerDependenciesMeta": {
|
| 1472 |
-
"@emotion/react": {
|
| 1473 |
-
"optional": true
|
| 1474 |
-
},
|
| 1475 |
-
"@emotion/styled": {
|
| 1476 |
-
"optional": true
|
| 1477 |
-
},
|
| 1478 |
-
"@types/react": {
|
| 1479 |
-
"optional": true
|
| 1480 |
-
}
|
| 1481 |
-
}
|
| 1482 |
-
},
|
| 1483 |
-
"node_modules/@mui/types": {
|
| 1484 |
-
"version": "9.0.0",
|
| 1485 |
-
"resolved": "https://registry.npmjs.org/@mui/types/-/types-9.0.0.tgz",
|
| 1486 |
-
"integrity": "sha512-i1cuFCAWN44b3AJWO7mh7tuh1sqbQSeVr/94oG0TX5uXivac8XalgE4/6fQZcmGZigzbQ35IXxj/4jLpRIBYZg==",
|
| 1487 |
-
"license": "MIT",
|
| 1488 |
-
"dependencies": {
|
| 1489 |
-
"@babel/runtime": "^7.29.2"
|
| 1490 |
-
},
|
| 1491 |
-
"peerDependencies": {
|
| 1492 |
-
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 1493 |
-
},
|
| 1494 |
-
"peerDependenciesMeta": {
|
| 1495 |
-
"@types/react": {
|
| 1496 |
-
"optional": true
|
| 1497 |
-
}
|
| 1498 |
-
}
|
| 1499 |
-
},
|
| 1500 |
-
"node_modules/@mui/utils": {
|
| 1501 |
-
"version": "9.0.0",
|
| 1502 |
-
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.0.0.tgz",
|
| 1503 |
-
"integrity": "sha512-bQcqyg/gjULUqTuyUjSAFr6LQGLvtkNtDbJerAtoUn9kGZ0hg5QJiN1PLHMLbeFpe3te1831uq7GFl2ITokGdg==",
|
| 1504 |
-
"license": "MIT",
|
| 1505 |
-
"dependencies": {
|
| 1506 |
-
"@babel/runtime": "^7.29.2",
|
| 1507 |
-
"@mui/types": "^9.0.0",
|
| 1508 |
-
"@types/prop-types": "^15.7.15",
|
| 1509 |
-
"clsx": "^2.1.1",
|
| 1510 |
-
"prop-types": "^15.8.1",
|
| 1511 |
-
"react-is": "^19.2.4"
|
| 1512 |
-
},
|
| 1513 |
-
"engines": {
|
| 1514 |
-
"node": ">=14.0.0"
|
| 1515 |
-
},
|
| 1516 |
-
"funding": {
|
| 1517 |
-
"type": "opencollective",
|
| 1518 |
-
"url": "https://opencollective.com/mui-org"
|
| 1519 |
-
},
|
| 1520 |
-
"peerDependencies": {
|
| 1521 |
-
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
| 1522 |
-
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 1523 |
-
},
|
| 1524 |
-
"peerDependenciesMeta": {
|
| 1525 |
-
"@types/react": {
|
| 1526 |
-
"optional": true
|
| 1527 |
-
}
|
| 1528 |
-
}
|
| 1529 |
-
},
|
| 1530 |
"node_modules/@opentelemetry/api": {
|
| 1531 |
"version": "1.9.0",
|
| 1532 |
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
|
@@ -2865,12 +2485,6 @@
|
|
| 2865 |
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
| 2866 |
"license": "MIT"
|
| 2867 |
},
|
| 2868 |
-
"node_modules/@types/parse-json": {
|
| 2869 |
-
"version": "4.0.2",
|
| 2870 |
-
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
| 2871 |
-
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
|
| 2872 |
-
"license": "MIT"
|
| 2873 |
-
},
|
| 2874 |
"node_modules/@types/prop-types": {
|
| 2875 |
"version": "15.7.15",
|
| 2876 |
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
|
@@ -2898,15 +2512,6 @@
|
|
| 2898 |
"@types/react": "^18.0.0"
|
| 2899 |
}
|
| 2900 |
},
|
| 2901 |
-
"node_modules/@types/react-transition-group": {
|
| 2902 |
-
"version": "4.4.12",
|
| 2903 |
-
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
|
| 2904 |
-
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
|
| 2905 |
-
"license": "MIT",
|
| 2906 |
-
"peerDependencies": {
|
| 2907 |
-
"@types/react": "*"
|
| 2908 |
-
}
|
| 2909 |
-
},
|
| 2910 |
"node_modules/@types/trusted-types": {
|
| 2911 |
"version": "2.0.7",
|
| 2912 |
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
|
@@ -3002,21 +2607,6 @@
|
|
| 3002 |
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
| 3003 |
"license": "Python-2.0"
|
| 3004 |
},
|
| 3005 |
-
"node_modules/babel-plugin-macros": {
|
| 3006 |
-
"version": "3.1.0",
|
| 3007 |
-
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
| 3008 |
-
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
|
| 3009 |
-
"license": "MIT",
|
| 3010 |
-
"dependencies": {
|
| 3011 |
-
"@babel/runtime": "^7.12.5",
|
| 3012 |
-
"cosmiconfig": "^7.0.0",
|
| 3013 |
-
"resolve": "^1.19.0"
|
| 3014 |
-
},
|
| 3015 |
-
"engines": {
|
| 3016 |
-
"node": ">=10",
|
| 3017 |
-
"npm": ">=6"
|
| 3018 |
-
}
|
| 3019 |
-
},
|
| 3020 |
"node_modules/baseline-browser-mapping": {
|
| 3021 |
"version": "2.10.18",
|
| 3022 |
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz",
|
|
@@ -3065,15 +2655,6 @@
|
|
| 3065 |
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 3066 |
}
|
| 3067 |
},
|
| 3068 |
-
"node_modules/callsites": {
|
| 3069 |
-
"version": "3.1.0",
|
| 3070 |
-
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
| 3071 |
-
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
| 3072 |
-
"license": "MIT",
|
| 3073 |
-
"engines": {
|
| 3074 |
-
"node": ">=6"
|
| 3075 |
-
}
|
| 3076 |
-
},
|
| 3077 |
"node_modules/caniuse-lite": {
|
| 3078 |
"version": "1.0.30001787",
|
| 3079 |
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
|
|
@@ -3124,15 +2705,6 @@
|
|
| 3124 |
"chevrotain": "^12.0.0"
|
| 3125 |
}
|
| 3126 |
},
|
| 3127 |
-
"node_modules/clsx": {
|
| 3128 |
-
"version": "2.1.1",
|
| 3129 |
-
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
| 3130 |
-
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
| 3131 |
-
"license": "MIT",
|
| 3132 |
-
"engines": {
|
| 3133 |
-
"node": ">=6"
|
| 3134 |
-
}
|
| 3135 |
-
},
|
| 3136 |
"node_modules/commander": {
|
| 3137 |
"version": "8.3.0",
|
| 3138 |
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
|
@@ -3148,12 +2720,6 @@
|
|
| 3148 |
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
|
| 3149 |
"license": "MIT"
|
| 3150 |
},
|
| 3151 |
-
"node_modules/convert-source-map": {
|
| 3152 |
-
"version": "1.9.0",
|
| 3153 |
-
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
|
| 3154 |
-
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
|
| 3155 |
-
"license": "MIT"
|
| 3156 |
-
},
|
| 3157 |
"node_modules/cose-base": {
|
| 3158 |
"version": "1.0.3",
|
| 3159 |
"resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
|
|
@@ -3163,31 +2729,6 @@
|
|
| 3163 |
"layout-base": "^1.0.0"
|
| 3164 |
}
|
| 3165 |
},
|
| 3166 |
-
"node_modules/cosmiconfig": {
|
| 3167 |
-
"version": "7.1.0",
|
| 3168 |
-
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
| 3169 |
-
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
|
| 3170 |
-
"license": "MIT",
|
| 3171 |
-
"dependencies": {
|
| 3172 |
-
"@types/parse-json": "^4.0.0",
|
| 3173 |
-
"import-fresh": "^3.2.1",
|
| 3174 |
-
"parse-json": "^5.0.0",
|
| 3175 |
-
"path-type": "^4.0.0",
|
| 3176 |
-
"yaml": "^1.10.0"
|
| 3177 |
-
},
|
| 3178 |
-
"engines": {
|
| 3179 |
-
"node": ">=10"
|
| 3180 |
-
}
|
| 3181 |
-
},
|
| 3182 |
-
"node_modules/cosmiconfig/node_modules/yaml": {
|
| 3183 |
-
"version": "1.10.3",
|
| 3184 |
-
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
|
| 3185 |
-
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
|
| 3186 |
-
"license": "ISC",
|
| 3187 |
-
"engines": {
|
| 3188 |
-
"node": ">= 6"
|
| 3189 |
-
}
|
| 3190 |
-
},
|
| 3191 |
"node_modules/crelt": {
|
| 3192 |
"version": "1.0.6",
|
| 3193 |
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
|
@@ -3720,6 +3261,7 @@
|
|
| 3720 |
"version": "4.4.3",
|
| 3721 |
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 3722 |
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
|
|
|
| 3723 |
"license": "MIT",
|
| 3724 |
"dependencies": {
|
| 3725 |
"ms": "^2.1.3"
|
|
@@ -3764,16 +3306,6 @@
|
|
| 3764 |
"url": "https://github.com/sponsors/wooorm"
|
| 3765 |
}
|
| 3766 |
},
|
| 3767 |
-
"node_modules/dom-helpers": {
|
| 3768 |
-
"version": "5.2.1",
|
| 3769 |
-
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
| 3770 |
-
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
| 3771 |
-
"license": "MIT",
|
| 3772 |
-
"dependencies": {
|
| 3773 |
-
"@babel/runtime": "^7.8.7",
|
| 3774 |
-
"csstype": "^3.0.2"
|
| 3775 |
-
}
|
| 3776 |
-
},
|
| 3777 |
"node_modules/dompurify": {
|
| 3778 |
"version": "3.3.3",
|
| 3779 |
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
|
|
@@ -3802,24 +3334,6 @@
|
|
| 3802 |
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 3803 |
}
|
| 3804 |
},
|
| 3805 |
-
"node_modules/error-ex": {
|
| 3806 |
-
"version": "1.3.4",
|
| 3807 |
-
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
| 3808 |
-
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
|
| 3809 |
-
"license": "MIT",
|
| 3810 |
-
"dependencies": {
|
| 3811 |
-
"is-arrayish": "^0.2.1"
|
| 3812 |
-
}
|
| 3813 |
-
},
|
| 3814 |
-
"node_modules/es-errors": {
|
| 3815 |
-
"version": "1.3.0",
|
| 3816 |
-
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
| 3817 |
-
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
| 3818 |
-
"license": "MIT",
|
| 3819 |
-
"engines": {
|
| 3820 |
-
"node": ">= 0.4"
|
| 3821 |
-
}
|
| 3822 |
-
},
|
| 3823 |
"node_modules/esbuild": {
|
| 3824 |
"version": "0.25.12",
|
| 3825 |
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
|
@@ -3920,12 +3434,6 @@
|
|
| 3920 |
}
|
| 3921 |
}
|
| 3922 |
},
|
| 3923 |
-
"node_modules/find-root": {
|
| 3924 |
-
"version": "1.1.0",
|
| 3925 |
-
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
| 3926 |
-
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
|
| 3927 |
-
"license": "MIT"
|
| 3928 |
-
},
|
| 3929 |
"node_modules/fsevents": {
|
| 3930 |
"version": "2.3.3",
|
| 3931 |
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
|
@@ -3941,15 +3449,6 @@
|
|
| 3941 |
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 3942 |
}
|
| 3943 |
},
|
| 3944 |
-
"node_modules/function-bind": {
|
| 3945 |
-
"version": "1.1.2",
|
| 3946 |
-
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
| 3947 |
-
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
| 3948 |
-
"license": "MIT",
|
| 3949 |
-
"funding": {
|
| 3950 |
-
"url": "https://github.com/sponsors/ljharb"
|
| 3951 |
-
}
|
| 3952 |
-
},
|
| 3953 |
"node_modules/gensync": {
|
| 3954 |
"version": "1.0.0-beta.2",
|
| 3955 |
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
|
@@ -3966,18 +3465,6 @@
|
|
| 3966 |
"integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==",
|
| 3967 |
"license": "MIT"
|
| 3968 |
},
|
| 3969 |
-
"node_modules/hasown": {
|
| 3970 |
-
"version": "2.0.2",
|
| 3971 |
-
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
| 3972 |
-
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
| 3973 |
-
"license": "MIT",
|
| 3974 |
-
"dependencies": {
|
| 3975 |
-
"function-bind": "^1.1.2"
|
| 3976 |
-
},
|
| 3977 |
-
"engines": {
|
| 3978 |
-
"node": ">= 0.4"
|
| 3979 |
-
}
|
| 3980 |
-
},
|
| 3981 |
"node_modules/highlight.js": {
|
| 3982 |
"version": "11.11.1",
|
| 3983 |
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
|
@@ -3988,21 +3475,6 @@
|
|
| 3988 |
"node": ">=12.0.0"
|
| 3989 |
}
|
| 3990 |
},
|
| 3991 |
-
"node_modules/hoist-non-react-statics": {
|
| 3992 |
-
"version": "3.3.2",
|
| 3993 |
-
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
| 3994 |
-
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
| 3995 |
-
"license": "BSD-3-Clause",
|
| 3996 |
-
"dependencies": {
|
| 3997 |
-
"react-is": "^16.7.0"
|
| 3998 |
-
}
|
| 3999 |
-
},
|
| 4000 |
-
"node_modules/hoist-non-react-statics/node_modules/react-is": {
|
| 4001 |
-
"version": "16.13.1",
|
| 4002 |
-
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
| 4003 |
-
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
| 4004 |
-
"license": "MIT"
|
| 4005 |
-
},
|
| 4006 |
"node_modules/iconv-lite": {
|
| 4007 |
"version": "0.6.3",
|
| 4008 |
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
|
@@ -4015,22 +3487,6 @@
|
|
| 4015 |
"node": ">=0.10.0"
|
| 4016 |
}
|
| 4017 |
},
|
| 4018 |
-
"node_modules/import-fresh": {
|
| 4019 |
-
"version": "3.3.1",
|
| 4020 |
-
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
| 4021 |
-
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
|
| 4022 |
-
"license": "MIT",
|
| 4023 |
-
"dependencies": {
|
| 4024 |
-
"parent-module": "^1.0.0",
|
| 4025 |
-
"resolve-from": "^4.0.0"
|
| 4026 |
-
},
|
| 4027 |
-
"engines": {
|
| 4028 |
-
"node": ">=6"
|
| 4029 |
-
},
|
| 4030 |
-
"funding": {
|
| 4031 |
-
"url": "https://github.com/sponsors/sindresorhus"
|
| 4032 |
-
}
|
| 4033 |
-
},
|
| 4034 |
"node_modules/internmap": {
|
| 4035 |
"version": "2.0.3",
|
| 4036 |
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
|
@@ -4040,27 +3496,6 @@
|
|
| 4040 |
"node": ">=12"
|
| 4041 |
}
|
| 4042 |
},
|
| 4043 |
-
"node_modules/is-arrayish": {
|
| 4044 |
-
"version": "0.2.1",
|
| 4045 |
-
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
| 4046 |
-
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
|
| 4047 |
-
"license": "MIT"
|
| 4048 |
-
},
|
| 4049 |
-
"node_modules/is-core-module": {
|
| 4050 |
-
"version": "2.16.1",
|
| 4051 |
-
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
| 4052 |
-
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
| 4053 |
-
"license": "MIT",
|
| 4054 |
-
"dependencies": {
|
| 4055 |
-
"hasown": "^2.0.2"
|
| 4056 |
-
},
|
| 4057 |
-
"engines": {
|
| 4058 |
-
"node": ">= 0.4"
|
| 4059 |
-
},
|
| 4060 |
-
"funding": {
|
| 4061 |
-
"url": "https://github.com/sponsors/ljharb"
|
| 4062 |
-
}
|
| 4063 |
-
},
|
| 4064 |
"node_modules/isomorphic.js": {
|
| 4065 |
"version": "0.2.5",
|
| 4066 |
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
|
|
@@ -4081,6 +3516,7 @@
|
|
| 4081 |
"version": "3.1.0",
|
| 4082 |
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
| 4083 |
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
|
|
|
| 4084 |
"license": "MIT",
|
| 4085 |
"bin": {
|
| 4086 |
"jsesc": "bin/jsesc"
|
|
@@ -4089,12 +3525,6 @@
|
|
| 4089 |
"node": ">=6"
|
| 4090 |
}
|
| 4091 |
},
|
| 4092 |
-
"node_modules/json-parse-even-better-errors": {
|
| 4093 |
-
"version": "2.3.1",
|
| 4094 |
-
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
| 4095 |
-
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
| 4096 |
-
"license": "MIT"
|
| 4097 |
-
},
|
| 4098 |
"node_modules/json-schema": {
|
| 4099 |
"version": "0.4.0",
|
| 4100 |
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
|
|
@@ -4181,12 +3611,6 @@
|
|
| 4181 |
"url": "https://github.com/sponsors/dmonad"
|
| 4182 |
}
|
| 4183 |
},
|
| 4184 |
-
"node_modules/lines-and-columns": {
|
| 4185 |
-
"version": "1.2.4",
|
| 4186 |
-
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
| 4187 |
-
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
| 4188 |
-
"license": "MIT"
|
| 4189 |
-
},
|
| 4190 |
"node_modules/linkify-it": {
|
| 4191 |
"version": "5.0.0",
|
| 4192 |
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
|
@@ -4246,6 +3670,15 @@
|
|
| 4246 |
"yallist": "^3.0.2"
|
| 4247 |
}
|
| 4248 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4249 |
"node_modules/markdown-it": {
|
| 4250 |
"version": "14.1.1",
|
| 4251 |
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
|
|
@@ -4332,6 +3765,7 @@
|
|
| 4332 |
"version": "2.1.3",
|
| 4333 |
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 4334 |
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
|
|
|
| 4335 |
"license": "MIT"
|
| 4336 |
},
|
| 4337 |
"node_modules/nanoid": {
|
|
@@ -4360,15 +3794,6 @@
|
|
| 4360 |
"dev": true,
|
| 4361 |
"license": "MIT"
|
| 4362 |
},
|
| 4363 |
-
"node_modules/object-assign": {
|
| 4364 |
-
"version": "4.1.1",
|
| 4365 |
-
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
| 4366 |
-
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
| 4367 |
-
"license": "MIT",
|
| 4368 |
-
"engines": {
|
| 4369 |
-
"node": ">=0.10.0"
|
| 4370 |
-
}
|
| 4371 |
-
},
|
| 4372 |
"node_modules/orderedmap": {
|
| 4373 |
"version": "2.1.1",
|
| 4374 |
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
|
@@ -4381,57 +3806,12 @@
|
|
| 4381 |
"integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==",
|
| 4382 |
"license": "MIT"
|
| 4383 |
},
|
| 4384 |
-
"node_modules/parent-module": {
|
| 4385 |
-
"version": "1.0.1",
|
| 4386 |
-
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
| 4387 |
-
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
|
| 4388 |
-
"license": "MIT",
|
| 4389 |
-
"dependencies": {
|
| 4390 |
-
"callsites": "^3.0.0"
|
| 4391 |
-
},
|
| 4392 |
-
"engines": {
|
| 4393 |
-
"node": ">=6"
|
| 4394 |
-
}
|
| 4395 |
-
},
|
| 4396 |
-
"node_modules/parse-json": {
|
| 4397 |
-
"version": "5.2.0",
|
| 4398 |
-
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
| 4399 |
-
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
|
| 4400 |
-
"license": "MIT",
|
| 4401 |
-
"dependencies": {
|
| 4402 |
-
"@babel/code-frame": "^7.0.0",
|
| 4403 |
-
"error-ex": "^1.3.1",
|
| 4404 |
-
"json-parse-even-better-errors": "^2.3.0",
|
| 4405 |
-
"lines-and-columns": "^1.1.6"
|
| 4406 |
-
},
|
| 4407 |
-
"engines": {
|
| 4408 |
-
"node": ">=8"
|
| 4409 |
-
},
|
| 4410 |
-
"funding": {
|
| 4411 |
-
"url": "https://github.com/sponsors/sindresorhus"
|
| 4412 |
-
}
|
| 4413 |
-
},
|
| 4414 |
"node_modules/path-data-parser": {
|
| 4415 |
"version": "0.1.0",
|
| 4416 |
"resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
|
| 4417 |
"integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
|
| 4418 |
"license": "MIT"
|
| 4419 |
},
|
| 4420 |
-
"node_modules/path-parse": {
|
| 4421 |
-
"version": "1.0.7",
|
| 4422 |
-
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
| 4423 |
-
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
| 4424 |
-
"license": "MIT"
|
| 4425 |
-
},
|
| 4426 |
-
"node_modules/path-type": {
|
| 4427 |
-
"version": "4.0.0",
|
| 4428 |
-
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
| 4429 |
-
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
| 4430 |
-
"license": "MIT",
|
| 4431 |
-
"engines": {
|
| 4432 |
-
"node": ">=8"
|
| 4433 |
-
}
|
| 4434 |
-
},
|
| 4435 |
"node_modules/pathe": {
|
| 4436 |
"version": "2.0.3",
|
| 4437 |
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
|
@@ -4442,6 +3822,7 @@
|
|
| 4442 |
"version": "1.1.1",
|
| 4443 |
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 4444 |
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
|
|
|
| 4445 |
"license": "ISC"
|
| 4446 |
},
|
| 4447 |
"node_modules/picomatch": {
|
|
@@ -4544,23 +3925,6 @@
|
|
| 4544 |
"postcss": "^8.4"
|
| 4545 |
}
|
| 4546 |
},
|
| 4547 |
-
"node_modules/prop-types": {
|
| 4548 |
-
"version": "15.8.1",
|
| 4549 |
-
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
| 4550 |
-
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
| 4551 |
-
"license": "MIT",
|
| 4552 |
-
"dependencies": {
|
| 4553 |
-
"loose-envify": "^1.4.0",
|
| 4554 |
-
"object-assign": "^4.1.1",
|
| 4555 |
-
"react-is": "^16.13.1"
|
| 4556 |
-
}
|
| 4557 |
-
},
|
| 4558 |
-
"node_modules/prop-types/node_modules/react-is": {
|
| 4559 |
-
"version": "16.13.1",
|
| 4560 |
-
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
| 4561 |
-
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
| 4562 |
-
"license": "MIT"
|
| 4563 |
-
},
|
| 4564 |
"node_modules/prosemirror-changeset": {
|
| 4565 |
"version": "2.4.0",
|
| 4566 |
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
|
|
@@ -4795,12 +4159,6 @@
|
|
| 4795 |
"react": "^18.3.1"
|
| 4796 |
}
|
| 4797 |
},
|
| 4798 |
-
"node_modules/react-is": {
|
| 4799 |
-
"version": "19.2.5",
|
| 4800 |
-
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz",
|
| 4801 |
-
"integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==",
|
| 4802 |
-
"license": "MIT"
|
| 4803 |
-
},
|
| 4804 |
"node_modules/react-refresh": {
|
| 4805 |
"version": "0.17.0",
|
| 4806 |
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
|
@@ -4811,52 +4169,6 @@
|
|
| 4811 |
"node": ">=0.10.0"
|
| 4812 |
}
|
| 4813 |
},
|
| 4814 |
-
"node_modules/react-transition-group": {
|
| 4815 |
-
"version": "4.4.5",
|
| 4816 |
-
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
| 4817 |
-
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
| 4818 |
-
"license": "BSD-3-Clause",
|
| 4819 |
-
"dependencies": {
|
| 4820 |
-
"@babel/runtime": "^7.5.5",
|
| 4821 |
-
"dom-helpers": "^5.0.1",
|
| 4822 |
-
"loose-envify": "^1.4.0",
|
| 4823 |
-
"prop-types": "^15.6.2"
|
| 4824 |
-
},
|
| 4825 |
-
"peerDependencies": {
|
| 4826 |
-
"react": ">=16.6.0",
|
| 4827 |
-
"react-dom": ">=16.6.0"
|
| 4828 |
-
}
|
| 4829 |
-
},
|
| 4830 |
-
"node_modules/resolve": {
|
| 4831 |
-
"version": "1.22.12",
|
| 4832 |
-
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
| 4833 |
-
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
|
| 4834 |
-
"license": "MIT",
|
| 4835 |
-
"dependencies": {
|
| 4836 |
-
"es-errors": "^1.3.0",
|
| 4837 |
-
"is-core-module": "^2.16.1",
|
| 4838 |
-
"path-parse": "^1.0.7",
|
| 4839 |
-
"supports-preserve-symlinks-flag": "^1.0.0"
|
| 4840 |
-
},
|
| 4841 |
-
"bin": {
|
| 4842 |
-
"resolve": "bin/resolve"
|
| 4843 |
-
},
|
| 4844 |
-
"engines": {
|
| 4845 |
-
"node": ">= 0.4"
|
| 4846 |
-
},
|
| 4847 |
-
"funding": {
|
| 4848 |
-
"url": "https://github.com/sponsors/ljharb"
|
| 4849 |
-
}
|
| 4850 |
-
},
|
| 4851 |
-
"node_modules/resolve-from": {
|
| 4852 |
-
"version": "4.0.0",
|
| 4853 |
-
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
| 4854 |
-
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
| 4855 |
-
"license": "MIT",
|
| 4856 |
-
"engines": {
|
| 4857 |
-
"node": ">=4"
|
| 4858 |
-
}
|
| 4859 |
-
},
|
| 4860 |
"node_modules/robust-predicates": {
|
| 4861 |
"version": "3.0.3",
|
| 4862 |
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
|
|
@@ -4957,15 +4269,6 @@
|
|
| 4957 |
"semver": "bin/semver.js"
|
| 4958 |
}
|
| 4959 |
},
|
| 4960 |
-
"node_modules/source-map": {
|
| 4961 |
-
"version": "0.5.7",
|
| 4962 |
-
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
| 4963 |
-
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
|
| 4964 |
-
"license": "BSD-3-Clause",
|
| 4965 |
-
"engines": {
|
| 4966 |
-
"node": ">=0.10.0"
|
| 4967 |
-
}
|
| 4968 |
-
},
|
| 4969 |
"node_modules/source-map-js": {
|
| 4970 |
"version": "1.2.1",
|
| 4971 |
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
|
@@ -4976,24 +4279,6 @@
|
|
| 4976 |
"node": ">=0.10.0"
|
| 4977 |
}
|
| 4978 |
},
|
| 4979 |
-
"node_modules/stylis": {
|
| 4980 |
-
"version": "4.2.0",
|
| 4981 |
-
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
|
| 4982 |
-
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
|
| 4983 |
-
"license": "MIT"
|
| 4984 |
-
},
|
| 4985 |
-
"node_modules/supports-preserve-symlinks-flag": {
|
| 4986 |
-
"version": "1.0.0",
|
| 4987 |
-
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
| 4988 |
-
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
| 4989 |
-
"license": "MIT",
|
| 4990 |
-
"engines": {
|
| 4991 |
-
"node": ">= 0.4"
|
| 4992 |
-
},
|
| 4993 |
-
"funding": {
|
| 4994 |
-
"url": "https://github.com/sponsors/ljharb"
|
| 4995 |
-
}
|
| 4996 |
-
},
|
| 4997 |
"node_modules/swr": {
|
| 4998 |
"version": "2.4.1",
|
| 4999 |
"resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz",
|
|
|
|
| 9 |
"version": "0.1.0",
|
| 10 |
"dependencies": {
|
| 11 |
"@ai-sdk/react": "^3.0.160",
|
|
|
|
|
|
|
| 12 |
"@floating-ui/dom": "^1.7.6",
|
| 13 |
"@hocuspocus/provider": "^3.4.4",
|
|
|
|
|
|
|
| 14 |
"@tiptap/core": "^3.22.3",
|
| 15 |
"@tiptap/extension-code-block-lowlight": "^3.22.3",
|
| 16 |
"@tiptap/extension-collaboration": "^3.22.3",
|
|
|
|
| 32 |
"ai": "^6.0.158",
|
| 33 |
"katex": "^0.16.45",
|
| 34 |
"lowlight": "^3.2.0",
|
| 35 |
+
"lucide-react": "^1.8.0",
|
| 36 |
"mermaid": "^11.14.0",
|
| 37 |
"react": "^18.3.0",
|
| 38 |
"react-dom": "^18.3.0",
|
|
|
|
| 129 |
"version": "7.29.0",
|
| 130 |
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
| 131 |
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
| 132 |
+
"dev": true,
|
| 133 |
"license": "MIT",
|
| 134 |
"dependencies": {
|
| 135 |
"@babel/helper-validator-identifier": "^7.28.5",
|
|
|
|
| 193 |
"version": "7.29.1",
|
| 194 |
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
|
| 195 |
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
| 196 |
+
"dev": true,
|
| 197 |
"license": "MIT",
|
| 198 |
"dependencies": {
|
| 199 |
"@babel/parser": "^7.29.0",
|
|
|
|
| 227 |
"version": "7.28.0",
|
| 228 |
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
| 229 |
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
| 230 |
+
"dev": true,
|
| 231 |
"license": "MIT",
|
| 232 |
"engines": {
|
| 233 |
"node": ">=6.9.0"
|
|
|
|
| 237 |
"version": "7.28.6",
|
| 238 |
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
|
| 239 |
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
|
| 240 |
+
"dev": true,
|
| 241 |
"license": "MIT",
|
| 242 |
"dependencies": {
|
| 243 |
"@babel/traverse": "^7.28.6",
|
|
|
|
| 279 |
"version": "7.27.1",
|
| 280 |
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
| 281 |
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
| 282 |
+
"dev": true,
|
| 283 |
"license": "MIT",
|
| 284 |
"engines": {
|
| 285 |
"node": ">=6.9.0"
|
|
|
|
| 289 |
"version": "7.28.5",
|
| 290 |
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
| 291 |
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
| 292 |
+
"dev": true,
|
| 293 |
"license": "MIT",
|
| 294 |
"engines": {
|
| 295 |
"node": ">=6.9.0"
|
|
|
|
| 323 |
"version": "7.29.2",
|
| 324 |
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
|
| 325 |
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
|
| 326 |
+
"dev": true,
|
| 327 |
"license": "MIT",
|
| 328 |
"dependencies": {
|
| 329 |
"@babel/types": "^7.29.0"
|
|
|
|
| 367 |
"@babel/core": "^7.0.0-0"
|
| 368 |
}
|
| 369 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
"node_modules/@babel/template": {
|
| 371 |
"version": "7.28.6",
|
| 372 |
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
| 373 |
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
| 374 |
+
"dev": true,
|
| 375 |
"license": "MIT",
|
| 376 |
"dependencies": {
|
| 377 |
"@babel/code-frame": "^7.28.6",
|
|
|
|
| 386 |
"version": "7.29.0",
|
| 387 |
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
|
| 388 |
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
| 389 |
+
"dev": true,
|
| 390 |
"license": "MIT",
|
| 391 |
"dependencies": {
|
| 392 |
"@babel/code-frame": "^7.29.0",
|
|
|
|
| 405 |
"version": "7.29.0",
|
| 406 |
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
| 407 |
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
| 408 |
+
"dev": true,
|
| 409 |
"license": "MIT",
|
| 410 |
"dependencies": {
|
| 411 |
"@babel/helper-string-parser": "^7.27.1",
|
|
|
|
| 551 |
"@csstools/css-tokenizer": "^4.0.0"
|
| 552 |
}
|
| 553 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
"node_modules/@esbuild/aix-ppc64": {
|
| 555 |
"version": "0.25.12",
|
| 556 |
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
|
|
|
| 1086 |
"version": "0.3.13",
|
| 1087 |
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
| 1088 |
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
| 1089 |
+
"dev": true,
|
| 1090 |
"license": "MIT",
|
| 1091 |
"dependencies": {
|
| 1092 |
"@jridgewell/sourcemap-codec": "^1.5.0",
|
|
|
|
| 1108 |
"version": "3.1.2",
|
| 1109 |
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
| 1110 |
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
| 1111 |
+
"dev": true,
|
| 1112 |
"license": "MIT",
|
| 1113 |
"engines": {
|
| 1114 |
"node": ">=6.0.0"
|
|
|
|
| 1118 |
"version": "1.5.5",
|
| 1119 |
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
| 1120 |
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
| 1121 |
+
"dev": true,
|
| 1122 |
"license": "MIT"
|
| 1123 |
},
|
| 1124 |
"node_modules/@jridgewell/trace-mapping": {
|
| 1125 |
"version": "0.3.31",
|
| 1126 |
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
| 1127 |
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
| 1128 |
+
"dev": true,
|
| 1129 |
"license": "MIT",
|
| 1130 |
"dependencies": {
|
| 1131 |
"@jridgewell/resolve-uri": "^3.1.0",
|
|
|
|
| 1147 |
"langium": "^4.0.0"
|
| 1148 |
}
|
| 1149 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1150 |
"node_modules/@opentelemetry/api": {
|
| 1151 |
"version": "1.9.0",
|
| 1152 |
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
|
|
|
| 2485 |
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
| 2486 |
"license": "MIT"
|
| 2487 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2488 |
"node_modules/@types/prop-types": {
|
| 2489 |
"version": "15.7.15",
|
| 2490 |
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
|
|
|
| 2512 |
"@types/react": "^18.0.0"
|
| 2513 |
}
|
| 2514 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2515 |
"node_modules/@types/trusted-types": {
|
| 2516 |
"version": "2.0.7",
|
| 2517 |
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
|
|
|
| 2607 |
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
| 2608 |
"license": "Python-2.0"
|
| 2609 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2610 |
"node_modules/baseline-browser-mapping": {
|
| 2611 |
"version": "2.10.18",
|
| 2612 |
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz",
|
|
|
|
| 2655 |
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 2656 |
}
|
| 2657 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2658 |
"node_modules/caniuse-lite": {
|
| 2659 |
"version": "1.0.30001787",
|
| 2660 |
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
|
|
|
|
| 2705 |
"chevrotain": "^12.0.0"
|
| 2706 |
}
|
| 2707 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2708 |
"node_modules/commander": {
|
| 2709 |
"version": "8.3.0",
|
| 2710 |
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
|
|
|
| 2720 |
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
|
| 2721 |
"license": "MIT"
|
| 2722 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2723 |
"node_modules/cose-base": {
|
| 2724 |
"version": "1.0.3",
|
| 2725 |
"resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
|
|
|
|
| 2729 |
"layout-base": "^1.0.0"
|
| 2730 |
}
|
| 2731 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2732 |
"node_modules/crelt": {
|
| 2733 |
"version": "1.0.6",
|
| 2734 |
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
|
|
|
| 3261 |
"version": "4.4.3",
|
| 3262 |
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 3263 |
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 3264 |
+
"dev": true,
|
| 3265 |
"license": "MIT",
|
| 3266 |
"dependencies": {
|
| 3267 |
"ms": "^2.1.3"
|
|
|
|
| 3306 |
"url": "https://github.com/sponsors/wooorm"
|
| 3307 |
}
|
| 3308 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3309 |
"node_modules/dompurify": {
|
| 3310 |
"version": "3.3.3",
|
| 3311 |
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
|
|
|
|
| 3334 |
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 3335 |
}
|
| 3336 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3337 |
"node_modules/esbuild": {
|
| 3338 |
"version": "0.25.12",
|
| 3339 |
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
|
|
|
| 3434 |
}
|
| 3435 |
}
|
| 3436 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3437 |
"node_modules/fsevents": {
|
| 3438 |
"version": "2.3.3",
|
| 3439 |
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
|
|
|
| 3449 |
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 3450 |
}
|
| 3451 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3452 |
"node_modules/gensync": {
|
| 3453 |
"version": "1.0.0-beta.2",
|
| 3454 |
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
|
|
|
| 3465 |
"integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==",
|
| 3466 |
"license": "MIT"
|
| 3467 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3468 |
"node_modules/highlight.js": {
|
| 3469 |
"version": "11.11.1",
|
| 3470 |
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
|
|
|
| 3475 |
"node": ">=12.0.0"
|
| 3476 |
}
|
| 3477 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3478 |
"node_modules/iconv-lite": {
|
| 3479 |
"version": "0.6.3",
|
| 3480 |
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
|
|
|
| 3487 |
"node": ">=0.10.0"
|
| 3488 |
}
|
| 3489 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3490 |
"node_modules/internmap": {
|
| 3491 |
"version": "2.0.3",
|
| 3492 |
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
|
|
|
| 3496 |
"node": ">=12"
|
| 3497 |
}
|
| 3498 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3499 |
"node_modules/isomorphic.js": {
|
| 3500 |
"version": "0.2.5",
|
| 3501 |
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
|
|
|
|
| 3516 |
"version": "3.1.0",
|
| 3517 |
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
| 3518 |
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
| 3519 |
+
"dev": true,
|
| 3520 |
"license": "MIT",
|
| 3521 |
"bin": {
|
| 3522 |
"jsesc": "bin/jsesc"
|
|
|
|
| 3525 |
"node": ">=6"
|
| 3526 |
}
|
| 3527 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3528 |
"node_modules/json-schema": {
|
| 3529 |
"version": "0.4.0",
|
| 3530 |
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
|
|
|
|
| 3611 |
"url": "https://github.com/sponsors/dmonad"
|
| 3612 |
}
|
| 3613 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3614 |
"node_modules/linkify-it": {
|
| 3615 |
"version": "5.0.0",
|
| 3616 |
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
|
|
|
| 3670 |
"yallist": "^3.0.2"
|
| 3671 |
}
|
| 3672 |
},
|
| 3673 |
+
"node_modules/lucide-react": {
|
| 3674 |
+
"version": "1.8.0",
|
| 3675 |
+
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz",
|
| 3676 |
+
"integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==",
|
| 3677 |
+
"license": "ISC",
|
| 3678 |
+
"peerDependencies": {
|
| 3679 |
+
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 3680 |
+
}
|
| 3681 |
+
},
|
| 3682 |
"node_modules/markdown-it": {
|
| 3683 |
"version": "14.1.1",
|
| 3684 |
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
|
|
|
|
| 3765 |
"version": "2.1.3",
|
| 3766 |
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 3767 |
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 3768 |
+
"dev": true,
|
| 3769 |
"license": "MIT"
|
| 3770 |
},
|
| 3771 |
"node_modules/nanoid": {
|
|
|
|
| 3794 |
"dev": true,
|
| 3795 |
"license": "MIT"
|
| 3796 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3797 |
"node_modules/orderedmap": {
|
| 3798 |
"version": "2.1.1",
|
| 3799 |
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
|
|
|
| 3806 |
"integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==",
|
| 3807 |
"license": "MIT"
|
| 3808 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3809 |
"node_modules/path-data-parser": {
|
| 3810 |
"version": "0.1.0",
|
| 3811 |
"resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
|
| 3812 |
"integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
|
| 3813 |
"license": "MIT"
|
| 3814 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3815 |
"node_modules/pathe": {
|
| 3816 |
"version": "2.0.3",
|
| 3817 |
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
|
|
|
| 3822 |
"version": "1.1.1",
|
| 3823 |
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 3824 |
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 3825 |
+
"dev": true,
|
| 3826 |
"license": "ISC"
|
| 3827 |
},
|
| 3828 |
"node_modules/picomatch": {
|
|
|
|
| 3925 |
"postcss": "^8.4"
|
| 3926 |
}
|
| 3927 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3928 |
"node_modules/prosemirror-changeset": {
|
| 3929 |
"version": "2.4.0",
|
| 3930 |
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
|
|
|
|
| 4159 |
"react": "^18.3.1"
|
| 4160 |
}
|
| 4161 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4162 |
"node_modules/react-refresh": {
|
| 4163 |
"version": "0.17.0",
|
| 4164 |
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
|
|
|
| 4169 |
"node": ">=0.10.0"
|
| 4170 |
}
|
| 4171 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4172 |
"node_modules/robust-predicates": {
|
| 4173 |
"version": "3.0.3",
|
| 4174 |
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
|
|
|
|
| 4269 |
"semver": "bin/semver.js"
|
| 4270 |
}
|
| 4271 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4272 |
"node_modules/source-map-js": {
|
| 4273 |
"version": "1.2.1",
|
| 4274 |
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
|
|
|
| 4279 |
"node": ">=0.10.0"
|
| 4280 |
}
|
| 4281 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4282 |
"node_modules/swr": {
|
| 4283 |
"version": "2.4.1",
|
| 4284 |
"resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz",
|
|
@@ -10,12 +10,8 @@
|
|
| 10 |
},
|
| 11 |
"dependencies": {
|
| 12 |
"@ai-sdk/react": "^3.0.160",
|
| 13 |
-
"@emotion/react": "^11.14.0",
|
| 14 |
-
"@emotion/styled": "^11.14.1",
|
| 15 |
"@floating-ui/dom": "^1.7.6",
|
| 16 |
"@hocuspocus/provider": "^3.4.4",
|
| 17 |
-
"@mui/icons-material": "^9.0.0",
|
| 18 |
-
"@mui/material": "^9.0.0",
|
| 19 |
"@tiptap/core": "^3.22.3",
|
| 20 |
"@tiptap/extension-code-block-lowlight": "^3.22.3",
|
| 21 |
"@tiptap/extension-collaboration": "^3.22.3",
|
|
@@ -37,6 +33,7 @@
|
|
| 37 |
"ai": "^6.0.158",
|
| 38 |
"katex": "^0.16.45",
|
| 39 |
"lowlight": "^3.2.0",
|
|
|
|
| 40 |
"mermaid": "^11.14.0",
|
| 41 |
"react": "^18.3.0",
|
| 42 |
"react-dom": "^18.3.0",
|
|
|
|
| 10 |
},
|
| 11 |
"dependencies": {
|
| 12 |
"@ai-sdk/react": "^3.0.160",
|
|
|
|
|
|
|
| 13 |
"@floating-ui/dom": "^1.7.6",
|
| 14 |
"@hocuspocus/provider": "^3.4.4",
|
|
|
|
|
|
|
| 15 |
"@tiptap/core": "^3.22.3",
|
| 16 |
"@tiptap/extension-code-block-lowlight": "^3.22.3",
|
| 17 |
"@tiptap/extension-collaboration": "^3.22.3",
|
|
|
|
| 33 |
"ai": "^6.0.158",
|
| 34 |
"katex": "^0.16.45",
|
| 35 |
"lowlight": "^3.2.0",
|
| 36 |
+
"lucide-react": "^1.8.0",
|
| 37 |
"mermaid": "^11.14.0",
|
| 38 |
"react": "^18.3.0",
|
| 39 |
"react-dom": "^18.3.0",
|
|
@@ -2,33 +2,16 @@ import { useRef, useState, useCallback, useEffect, useMemo } from "react";
|
|
| 2 |
import { Editor as TiptapEditor } from "@tiptap/core";
|
| 3 |
import { UndoManager } from "yjs";
|
| 4 |
import type * as Y from "yjs";
|
| 5 |
-
import {
|
| 6 |
import {
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
} from "
|
| 14 |
-
import UndoIcon from "@mui/icons-material/Undo";
|
| 15 |
-
import RedoIcon from "@mui/icons-material/Redo";
|
| 16 |
-
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
|
| 17 |
-
import PublishOutlinedIcon from "@mui/icons-material/PublishOutlined";
|
| 18 |
-
import { buildTheme, DEFAULT_PRIMARY, DEFAULT_HUE } from "./theme";
|
| 19 |
import { oklchToHex, OKLCH_L, OKLCH_C } from "./editor/frontmatter/HueSlider";
|
| 20 |
-
import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutlined";
|
| 21 |
-
import CloseIcon from "@mui/icons-material/Close";
|
| 22 |
-
import {
|
| 23 |
-
Button,
|
| 24 |
-
Dialog,
|
| 25 |
-
DialogTitle,
|
| 26 |
-
DialogContent,
|
| 27 |
-
DialogContentText,
|
| 28 |
-
DialogActions,
|
| 29 |
-
CircularProgress,
|
| 30 |
-
Badge,
|
| 31 |
-
} from "@mui/material";
|
| 32 |
import { Editor } from "./editor/Editor";
|
| 33 |
import { CommentsSidebar } from "./components/CommentsSidebar";
|
| 34 |
import { CommentDialog } from "./components/CommentDialog";
|
|
@@ -40,6 +23,8 @@ import type { FrontmatterStore } from "./editor/frontmatter/frontmatter-store";
|
|
| 40 |
import { FrontmatterHero } from "./editor/frontmatter/FrontmatterHero";
|
| 41 |
import { SettingsDrawer } from "./editor/frontmatter/SettingsDrawer";
|
| 42 |
|
|
|
|
|
|
|
| 43 |
const COLORS = [
|
| 44 |
"#958DF1", "#F98181", "#FBBC88", "#FAF594",
|
| 45 |
"#70CFF8", "#94FADB", "#B9F18D", "#C4B5FD",
|
|
@@ -85,6 +70,7 @@ export default function App() {
|
|
| 85 |
|
| 86 |
const editorRef = useRef<TiptapEditor | null>(null);
|
| 87 |
const editorContainerRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
| 88 |
const [editorInstance, setEditorInstance] = useState<TiptapEditor | null>(null);
|
| 89 |
const [containerEl, setContainerEl] = useState<HTMLElement | null>(null);
|
| 90 |
const [commentStore, setCommentStore] = useState<CommentStore | null>(null);
|
|
@@ -92,25 +78,19 @@ export default function App() {
|
|
| 92 |
const [settingsMap, setSettingsMap] = useState<Y.Map<any> | null>(null);
|
| 93 |
const [commentDialogOpen, setCommentDialogOpen] = useState(false);
|
| 94 |
const [settingsOpen, setSettingsOpen] = useState(false);
|
| 95 |
-
const [publishDialogOpen, setPublishDialogOpen] = useState(false);
|
| 96 |
const [publishState, setPublishState] = useState<"idle" | "loading" | "success" | "error">("idle");
|
| 97 |
const [publishError, setPublishError] = useState("");
|
| 98 |
const selectionRange = useRef<{ from: number; to: number } | null>(null);
|
| 99 |
const [hasSelection, setHasSelection] = useState(false);
|
| 100 |
const [undoManager, setUndoManager] = useState<UndoManager | null>(null);
|
| 101 |
const [chatOpen, setChatOpen] = useState(false);
|
| 102 |
-
const [primaryHue, setPrimaryHue] = useState(DEFAULT_HUE);
|
| 103 |
-
|
| 104 |
-
const primaryHex = useMemo(() => oklchToHex(OKLCH_L, OKLCH_C, primaryHue), [primaryHue]);
|
| 105 |
-
const muiTheme = useMemo(() => buildTheme(primaryHex), [primaryHex]);
|
| 106 |
|
| 107 |
-
// Sync primary hue from Yjs settings
|
| 108 |
useEffect(() => {
|
| 109 |
if (!settingsMap) return;
|
| 110 |
const sync = () => {
|
| 111 |
const h = settingsMap.get("primaryHue") as number | undefined;
|
| 112 |
if (h !== undefined) {
|
| 113 |
-
setPrimaryHue(h);
|
| 114 |
const oklch = `oklch(${OKLCH_L} ${OKLCH_C} ${h})`;
|
| 115 |
document.documentElement.style.setProperty("--primary-color", oklch);
|
| 116 |
document.documentElement.style.setProperty("--primary-color-hover", oklch);
|
|
@@ -155,6 +135,16 @@ export default function App() {
|
|
| 155 |
setSettingsMap(map);
|
| 156 |
}, []);
|
| 157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
const handlePublish = useCallback(async () => {
|
| 159 |
setPublishState("loading");
|
| 160 |
setPublishError("");
|
|
@@ -214,81 +204,64 @@ export default function App() {
|
|
| 214 |
}, [commentStore, user]);
|
| 215 |
|
| 216 |
return (
|
| 217 |
-
<
|
| 218 |
-
<Box
|
| 219 |
className="editor-app"
|
| 220 |
-
|
| 221 |
height: "100vh",
|
| 222 |
display: "flex",
|
| 223 |
flexDirection: "column",
|
| 224 |
-
|
| 225 |
overflow: "hidden",
|
| 226 |
}}
|
| 227 |
>
|
| 228 |
-
{/* Top bar
|
| 229 |
-
<
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
alignItems: "center",
|
| 234 |
-
justifyContent: "flex-end",
|
| 235 |
-
px: 2,
|
| 236 |
-
py: 0.75,
|
| 237 |
-
gap: 0.5,
|
| 238 |
-
}}
|
| 239 |
-
>
|
| 240 |
-
<Tooltip title="Undo" arrow>
|
| 241 |
-
<IconButton
|
| 242 |
-
size="small"
|
| 243 |
onClick={() => editorInstance?.commands.undo()}
|
| 244 |
-
|
| 245 |
>
|
| 246 |
-
<
|
| 247 |
-
</
|
| 248 |
</Tooltip>
|
| 249 |
-
<Tooltip title="Redo"
|
| 250 |
-
<
|
| 251 |
-
|
| 252 |
onClick={() => editorInstance?.commands.redo()}
|
| 253 |
-
|
| 254 |
>
|
| 255 |
-
<
|
| 256 |
-
</
|
| 257 |
</Tooltip>
|
| 258 |
-
<
|
| 259 |
-
<Tooltip title="Article settings"
|
| 260 |
-
<
|
| 261 |
-
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
</Tooltip>
|
| 264 |
-
<
|
| 265 |
-
<Tooltip title="Publish article"
|
| 266 |
-
<
|
| 267 |
-
|
| 268 |
-
onClick={
|
| 269 |
-
|
| 270 |
>
|
| 271 |
-
<
|
| 272 |
-
</
|
| 273 |
</Tooltip>
|
| 274 |
-
<
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
}
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
sx={{
|
| 283 |
-
bgcolor: user.color,
|
| 284 |
-
color: "#000",
|
| 285 |
-
fontWeight: 600,
|
| 286 |
-
fontSize: "0.65rem",
|
| 287 |
-
height: 22,
|
| 288 |
-
ml: 0.5,
|
| 289 |
-
}}
|
| 290 |
-
/>
|
| 291 |
-
</Box>
|
| 292 |
|
| 293 |
{/* Single scroll container */}
|
| 294 |
<div
|
|
@@ -296,19 +269,14 @@ export default function App() {
|
|
| 296 |
style={{ flex: 1, overflowY: "auto", overflowX: "hidden" }}
|
| 297 |
>
|
| 298 |
<div className="content-grid">
|
| 299 |
-
{/* Hero - center column only, row 1 */}
|
| 300 |
<div className="content-grid__hero">
|
| 301 |
<FrontmatterHero store={frontmatterStore} />
|
| 302 |
</div>
|
| 303 |
-
|
| 304 |
-
{/* TOC - left column, row 2, sticky */}
|
| 305 |
<div className="content-grid__toc">
|
| 306 |
<div className="table-of-contents--sticky">
|
| 307 |
<TableOfContents editor={editorInstance} />
|
| 308 |
</div>
|
| 309 |
</div>
|
| 310 |
-
|
| 311 |
-
{/* Editor - center column, row 2 */}
|
| 312 |
<div className="content-grid__editor">
|
| 313 |
<Editor
|
| 314 |
docName={docName}
|
|
@@ -322,8 +290,6 @@ export default function App() {
|
|
| 322 |
onAddComment={handleAddComment}
|
| 323 |
/>
|
| 324 |
</div>
|
| 325 |
-
|
| 326 |
-
{/* Comments - right column, row 2 */}
|
| 327 |
<div className="content-grid__comments">
|
| 328 |
<CommentsSidebar
|
| 329 |
editor={editorInstance}
|
|
@@ -335,47 +301,16 @@ export default function App() {
|
|
| 335 |
</div>
|
| 336 |
</div>
|
| 337 |
|
| 338 |
-
{/* Floating chat
|
| 339 |
{chatOpen ? (
|
| 340 |
-
<
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
borderRadius: 3,
|
| 349 |
-
overflow: "hidden",
|
| 350 |
-
display: "flex",
|
| 351 |
-
flexDirection: "column",
|
| 352 |
-
bgcolor: "background.paper",
|
| 353 |
-
border: "1px solid",
|
| 354 |
-
borderColor: "divider",
|
| 355 |
-
boxShadow: "0 8px 32px rgba(0,0,0,0.3)",
|
| 356 |
-
zIndex: 1300,
|
| 357 |
-
}}
|
| 358 |
-
>
|
| 359 |
-
<Box
|
| 360 |
-
sx={{
|
| 361 |
-
display: "flex",
|
| 362 |
-
alignItems: "center",
|
| 363 |
-
justifyContent: "space-between",
|
| 364 |
-
px: 1.5,
|
| 365 |
-
py: 0.75,
|
| 366 |
-
borderBottom: "1px solid",
|
| 367 |
-
borderColor: "divider",
|
| 368 |
-
flexShrink: 0,
|
| 369 |
-
}}
|
| 370 |
-
>
|
| 371 |
-
<Typography variant="caption" sx={{ fontWeight: 600, color: "text.secondary", fontSize: "0.7rem" }}>
|
| 372 |
-
AI Assistant
|
| 373 |
-
</Typography>
|
| 374 |
-
<IconButton size="small" onClick={() => setChatOpen(false)} sx={{ color: "text.disabled" }}>
|
| 375 |
-
<CloseIcon sx={{ fontSize: 16 }} />
|
| 376 |
-
</IconButton>
|
| 377 |
-
</Box>
|
| 378 |
-
<Box sx={{ flex: 1, overflow: "hidden", display: "flex" }}>
|
| 379 |
<ChatPanel
|
| 380 |
messages={agentChat.messages}
|
| 381 |
isLoading={agentChat.isLoading}
|
|
@@ -388,33 +323,17 @@ export default function App() {
|
|
| 388 |
onStop={agentChat.stop}
|
| 389 |
onNewChat={() => window.location.reload()}
|
| 390 |
/>
|
| 391 |
-
</
|
| 392 |
-
</
|
| 393 |
) : (
|
| 394 |
-
<Tooltip title="AI Assistant" placement="right"
|
| 395 |
-
<
|
|
|
|
| 396 |
onClick={() => setChatOpen(true)}
|
| 397 |
-
|
| 398 |
-
position: "fixed",
|
| 399 |
-
bottom: 20,
|
| 400 |
-
left: 20,
|
| 401 |
-
width: 48,
|
| 402 |
-
height: 48,
|
| 403 |
-
bgcolor: "primary.main",
|
| 404 |
-
color: "primary.contrastText",
|
| 405 |
-
boxShadow: "0 4px 16px rgba(0,0,0,0.3)",
|
| 406 |
-
zIndex: 1300,
|
| 407 |
-
"&:hover": { bgcolor: "primary.dark" },
|
| 408 |
-
}}
|
| 409 |
>
|
| 410 |
-
<
|
| 411 |
-
|
| 412 |
-
variant="dot"
|
| 413 |
-
invisible={!agentChat.isLoading}
|
| 414 |
-
>
|
| 415 |
-
<ChatBubbleOutlineIcon sx={{ fontSize: 22 }} />
|
| 416 |
-
</Badge>
|
| 417 |
-
</IconButton>
|
| 418 |
</Tooltip>
|
| 419 |
)}
|
| 420 |
|
|
@@ -431,59 +350,53 @@ export default function App() {
|
|
| 431 |
settingsMap={settingsMap}
|
| 432 |
/>
|
| 433 |
|
| 434 |
-
<
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
>
|
| 440 |
-
<
|
| 441 |
{publishState === "success" ? "Published!" : "Publish article"}
|
| 442 |
-
</
|
| 443 |
-
<
|
| 444 |
{publishState === "idle" && (
|
| 445 |
-
<
|
| 446 |
This will generate a static HTML page, PDF, and thumbnail
|
| 447 |
from the current document. Visitors without edit access
|
| 448 |
will see the published version.
|
| 449 |
-
</
|
| 450 |
)}
|
| 451 |
{publishState === "loading" && (
|
| 452 |
-
<
|
| 453 |
-
<
|
| 454 |
-
<
|
| 455 |
-
|
| 456 |
-
</DialogContentText>
|
| 457 |
-
</Box>
|
| 458 |
)}
|
| 459 |
{publishState === "success" && (
|
| 460 |
-
<
|
| 461 |
Article published successfully. Visitors will now see
|
| 462 |
the updated version.
|
| 463 |
-
</
|
| 464 |
)}
|
| 465 |
{publishState === "error" && (
|
| 466 |
-
<
|
| 467 |
{publishError || "An error occurred while publishing."}
|
| 468 |
-
</
|
| 469 |
)}
|
| 470 |
-
</
|
| 471 |
-
<
|
| 472 |
{publishState === "idle" && (
|
| 473 |
<>
|
| 474 |
-
<
|
| 475 |
-
<
|
| 476 |
-
Publish
|
| 477 |
-
</Button>
|
| 478 |
</>
|
| 479 |
)}
|
| 480 |
-
{publishState === "loading" && null}
|
| 481 |
{(publishState === "success" || publishState === "error") && (
|
| 482 |
-
<
|
| 483 |
)}
|
| 484 |
-
</
|
| 485 |
-
</
|
| 486 |
-
</
|
| 487 |
-
</ThemeProvider>
|
| 488 |
);
|
| 489 |
}
|
|
|
|
| 2 |
import { Editor as TiptapEditor } from "@tiptap/core";
|
| 3 |
import { UndoManager } from "yjs";
|
| 4 |
import type * as Y from "yjs";
|
| 5 |
+
import { Tooltip } from "./components/Tooltip";
|
| 6 |
import {
|
| 7 |
+
Undo2,
|
| 8 |
+
Redo2,
|
| 9 |
+
Settings,
|
| 10 |
+
Upload,
|
| 11 |
+
MessageCircle,
|
| 12 |
+
X,
|
| 13 |
+
} from "lucide-react";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
import { oklchToHex, OKLCH_L, OKLCH_C } from "./editor/frontmatter/HueSlider";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
import { Editor } from "./editor/Editor";
|
| 16 |
import { CommentsSidebar } from "./components/CommentsSidebar";
|
| 17 |
import { CommentDialog } from "./components/CommentDialog";
|
|
|
|
| 23 |
import { FrontmatterHero } from "./editor/frontmatter/FrontmatterHero";
|
| 24 |
import { SettingsDrawer } from "./editor/frontmatter/SettingsDrawer";
|
| 25 |
|
| 26 |
+
const DEFAULT_HUE = 47;
|
| 27 |
+
|
| 28 |
const COLORS = [
|
| 29 |
"#958DF1", "#F98181", "#FBBC88", "#FAF594",
|
| 30 |
"#70CFF8", "#94FADB", "#B9F18D", "#C4B5FD",
|
|
|
|
| 70 |
|
| 71 |
const editorRef = useRef<TiptapEditor | null>(null);
|
| 72 |
const editorContainerRef = useRef<HTMLDivElement | null>(null);
|
| 73 |
+
const publishDialogRef = useRef<HTMLDialogElement>(null);
|
| 74 |
const [editorInstance, setEditorInstance] = useState<TiptapEditor | null>(null);
|
| 75 |
const [containerEl, setContainerEl] = useState<HTMLElement | null>(null);
|
| 76 |
const [commentStore, setCommentStore] = useState<CommentStore | null>(null);
|
|
|
|
| 78 |
const [settingsMap, setSettingsMap] = useState<Y.Map<any> | null>(null);
|
| 79 |
const [commentDialogOpen, setCommentDialogOpen] = useState(false);
|
| 80 |
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
|
|
| 81 |
const [publishState, setPublishState] = useState<"idle" | "loading" | "success" | "error">("idle");
|
| 82 |
const [publishError, setPublishError] = useState("");
|
| 83 |
const selectionRange = useRef<{ from: number; to: number } | null>(null);
|
| 84 |
const [hasSelection, setHasSelection] = useState(false);
|
| 85 |
const [undoManager, setUndoManager] = useState<UndoManager | null>(null);
|
| 86 |
const [chatOpen, setChatOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
+
// Sync primary hue from Yjs settings to CSS variables
|
| 89 |
useEffect(() => {
|
| 90 |
if (!settingsMap) return;
|
| 91 |
const sync = () => {
|
| 92 |
const h = settingsMap.get("primaryHue") as number | undefined;
|
| 93 |
if (h !== undefined) {
|
|
|
|
| 94 |
const oklch = `oklch(${OKLCH_L} ${OKLCH_C} ${h})`;
|
| 95 |
document.documentElement.style.setProperty("--primary-color", oklch);
|
| 96 |
document.documentElement.style.setProperty("--primary-color-hover", oklch);
|
|
|
|
| 135 |
setSettingsMap(map);
|
| 136 |
}, []);
|
| 137 |
|
| 138 |
+
const openPublishDialog = useCallback(() => {
|
| 139 |
+
setPublishState("idle");
|
| 140 |
+
setPublishError("");
|
| 141 |
+
publishDialogRef.current?.showModal();
|
| 142 |
+
}, []);
|
| 143 |
+
|
| 144 |
+
const closePublishDialog = useCallback(() => {
|
| 145 |
+
publishDialogRef.current?.close();
|
| 146 |
+
}, []);
|
| 147 |
+
|
| 148 |
const handlePublish = useCallback(async () => {
|
| 149 |
setPublishState("loading");
|
| 150 |
setPublishError("");
|
|
|
|
| 204 |
}, [commentStore, user]);
|
| 205 |
|
| 206 |
return (
|
| 207 |
+
<div
|
|
|
|
| 208 |
className="editor-app"
|
| 209 |
+
style={{
|
| 210 |
height: "100vh",
|
| 211 |
display: "flex",
|
| 212 |
flexDirection: "column",
|
| 213 |
+
background: "var(--ed-bg)",
|
| 214 |
overflow: "hidden",
|
| 215 |
}}
|
| 216 |
>
|
| 217 |
+
{/* Top bar */}
|
| 218 |
+
<div className="top-bar">
|
| 219 |
+
<Tooltip title="Undo">
|
| 220 |
+
<button
|
| 221 |
+
className="icon-btn"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
onClick={() => editorInstance?.commands.undo()}
|
| 223 |
+
aria-label="Undo"
|
| 224 |
>
|
| 225 |
+
<Undo2 size={18} />
|
| 226 |
+
</button>
|
| 227 |
</Tooltip>
|
| 228 |
+
<Tooltip title="Redo">
|
| 229 |
+
<button
|
| 230 |
+
className="icon-btn"
|
| 231 |
onClick={() => editorInstance?.commands.redo()}
|
| 232 |
+
aria-label="Redo"
|
| 233 |
>
|
| 234 |
+
<Redo2 size={18} />
|
| 235 |
+
</button>
|
| 236 |
</Tooltip>
|
| 237 |
+
<span className="divider-v" />
|
| 238 |
+
<Tooltip title="Article settings">
|
| 239 |
+
<button
|
| 240 |
+
className="icon-btn"
|
| 241 |
+
onClick={() => setSettingsOpen(true)}
|
| 242 |
+
aria-label="Article settings"
|
| 243 |
+
>
|
| 244 |
+
<Settings size={18} />
|
| 245 |
+
</button>
|
| 246 |
</Tooltip>
|
| 247 |
+
<span className="divider-v" />
|
| 248 |
+
<Tooltip title="Publish article">
|
| 249 |
+
<button
|
| 250 |
+
className="icon-btn icon-btn--primary"
|
| 251 |
+
onClick={openPublishDialog}
|
| 252 |
+
aria-label="Publish article"
|
| 253 |
>
|
| 254 |
+
<Upload size={18} />
|
| 255 |
+
</button>
|
| 256 |
</Tooltip>
|
| 257 |
+
<span
|
| 258 |
+
className="chip"
|
| 259 |
+
style={{ backgroundColor: user.color, color: "#000", marginLeft: 4 }}
|
| 260 |
+
>
|
| 261 |
+
{user.avatarUrl && <img src={user.avatarUrl} alt="" />}
|
| 262 |
+
{user.name}
|
| 263 |
+
</span>
|
| 264 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
|
| 266 |
{/* Single scroll container */}
|
| 267 |
<div
|
|
|
|
| 269 |
style={{ flex: 1, overflowY: "auto", overflowX: "hidden" }}
|
| 270 |
>
|
| 271 |
<div className="content-grid">
|
|
|
|
| 272 |
<div className="content-grid__hero">
|
| 273 |
<FrontmatterHero store={frontmatterStore} />
|
| 274 |
</div>
|
|
|
|
|
|
|
| 275 |
<div className="content-grid__toc">
|
| 276 |
<div className="table-of-contents--sticky">
|
| 277 |
<TableOfContents editor={editorInstance} />
|
| 278 |
</div>
|
| 279 |
</div>
|
|
|
|
|
|
|
| 280 |
<div className="content-grid__editor">
|
| 281 |
<Editor
|
| 282 |
docName={docName}
|
|
|
|
| 290 |
onAddComment={handleAddComment}
|
| 291 |
/>
|
| 292 |
</div>
|
|
|
|
|
|
|
| 293 |
<div className="content-grid__comments">
|
| 294 |
<CommentsSidebar
|
| 295 |
editor={editorInstance}
|
|
|
|
| 301 |
</div>
|
| 302 |
</div>
|
| 303 |
|
| 304 |
+
{/* Floating chat */}
|
| 305 |
{chatOpen ? (
|
| 306 |
+
<div className="chat-floating">
|
| 307 |
+
<div className="chat-floating__header">
|
| 308 |
+
<span className="chat-floating__title">AI Assistant</span>
|
| 309 |
+
<button className="icon-btn" onClick={() => setChatOpen(false)} aria-label="Close chat">
|
| 310 |
+
<X size={16} />
|
| 311 |
+
</button>
|
| 312 |
+
</div>
|
| 313 |
+
<div className="chat-floating__body">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
<ChatPanel
|
| 315 |
messages={agentChat.messages}
|
| 316 |
isLoading={agentChat.isLoading}
|
|
|
|
| 323 |
onStop={agentChat.stop}
|
| 324 |
onNewChat={() => window.location.reload()}
|
| 325 |
/>
|
| 326 |
+
</div>
|
| 327 |
+
</div>
|
| 328 |
) : (
|
| 329 |
+
<Tooltip title="AI Assistant" placement="right">
|
| 330 |
+
<button
|
| 331 |
+
className={`chat-fab ${agentChat.isLoading ? "badge-dot" : "badge-dot badge-dot--hidden"}`}
|
| 332 |
onClick={() => setChatOpen(true)}
|
| 333 |
+
aria-label="AI Assistant"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
>
|
| 335 |
+
<MessageCircle size={22} />
|
| 336 |
+
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
</Tooltip>
|
| 338 |
)}
|
| 339 |
|
|
|
|
| 350 |
settingsMap={settingsMap}
|
| 351 |
/>
|
| 352 |
|
| 353 |
+
{/* Publish dialog (native <dialog>) */}
|
| 354 |
+
<dialog
|
| 355 |
+
ref={publishDialogRef}
|
| 356 |
+
className="ed-dialog"
|
| 357 |
+
onClose={closePublishDialog}
|
| 358 |
>
|
| 359 |
+
<h2 className="ed-dialog__title">
|
| 360 |
{publishState === "success" ? "Published!" : "Publish article"}
|
| 361 |
+
</h2>
|
| 362 |
+
<div className="ed-dialog__body">
|
| 363 |
{publishState === "idle" && (
|
| 364 |
+
<p>
|
| 365 |
This will generate a static HTML page, PDF, and thumbnail
|
| 366 |
from the current document. Visitors without edit access
|
| 367 |
will see the published version.
|
| 368 |
+
</p>
|
| 369 |
)}
|
| 370 |
{publishState === "loading" && (
|
| 371 |
+
<div style={{ display: "flex", alignItems: "center", gap: 12, padding: "8px 0" }}>
|
| 372 |
+
<span className="spinner" />
|
| 373 |
+
<p style={{ margin: 0 }}>Publishing...</p>
|
| 374 |
+
</div>
|
|
|
|
|
|
|
| 375 |
)}
|
| 376 |
{publishState === "success" && (
|
| 377 |
+
<p>
|
| 378 |
Article published successfully. Visitors will now see
|
| 379 |
the updated version.
|
| 380 |
+
</p>
|
| 381 |
)}
|
| 382 |
{publishState === "error" && (
|
| 383 |
+
<p style={{ color: "var(--ed-error)" }}>
|
| 384 |
{publishError || "An error occurred while publishing."}
|
| 385 |
+
</p>
|
| 386 |
)}
|
| 387 |
+
</div>
|
| 388 |
+
<div className="ed-dialog__actions">
|
| 389 |
{publishState === "idle" && (
|
| 390 |
<>
|
| 391 |
+
<button className="btn" onClick={closePublishDialog}>Cancel</button>
|
| 392 |
+
<button className="btn btn--primary" onClick={handlePublish}>Publish</button>
|
|
|
|
|
|
|
| 393 |
</>
|
| 394 |
)}
|
|
|
|
| 395 |
{(publishState === "success" || publishState === "error") && (
|
| 396 |
+
<button className="btn" onClick={closePublishDialog}>Close</button>
|
| 397 |
)}
|
| 398 |
+
</div>
|
| 399 |
+
</dialog>
|
| 400 |
+
</div>
|
|
|
|
| 401 |
);
|
| 402 |
}
|
|
@@ -1,22 +1,16 @@
|
|
| 1 |
import { useState, useRef, useEffect, type KeyboardEvent } from "react";
|
|
|
|
| 2 |
import {
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
import AddIcon from "@mui/icons-material/Add";
|
| 14 |
-
import AutoFixHighIcon from "@mui/icons-material/AutoFixHigh";
|
| 15 |
-
import ShortTextIcon from "@mui/icons-material/ShortText";
|
| 16 |
-
import ExpandIcon from "@mui/icons-material/Expand";
|
| 17 |
-
import SpellcheckIcon from "@mui/icons-material/Spellcheck";
|
| 18 |
-
import TranslateIcon from "@mui/icons-material/Translate";
|
| 19 |
-
import CompressIcon from "@mui/icons-material/Compress";
|
| 20 |
import type { UIMessage } from "ai";
|
| 21 |
|
| 22 |
interface ChatPanelProps {
|
|
@@ -33,12 +27,12 @@ interface ChatPanelProps {
|
|
| 33 |
}
|
| 34 |
|
| 35 |
const QUICK_ACTIONS = [
|
| 36 |
-
{ id: "rewrite", label: "Rewrite",
|
| 37 |
-
{ id: "expand", label: "Expand",
|
| 38 |
-
{ id: "summarize", label: "Summarize",
|
| 39 |
-
{ id: "fix-grammar", label: "Fix grammar",
|
| 40 |
-
{ id: "translate", label: "Translate",
|
| 41 |
-
{ id: "simplify", label: "Simplify",
|
| 42 |
];
|
| 43 |
|
| 44 |
export function ChatPanel({
|
|
@@ -77,56 +71,23 @@ export function ChatPanel({
|
|
| 77 |
};
|
| 78 |
|
| 79 |
return (
|
| 80 |
-
<
|
| 81 |
-
sx={{
|
| 82 |
-
display: "flex",
|
| 83 |
-
flexDirection: "column",
|
| 84 |
-
height: "100%",
|
| 85 |
-
width: "100%",
|
| 86 |
-
}}
|
| 87 |
-
>
|
| 88 |
{/* Header */}
|
| 89 |
-
<
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
py: 1,
|
| 96 |
-
borderBottom: "1px solid",
|
| 97 |
-
borderColor: "divider",
|
| 98 |
-
flexShrink: 0,
|
| 99 |
-
}}
|
| 100 |
-
>
|
| 101 |
-
<Typography variant="caption" sx={{ fontWeight: 600, color: "text.secondary", letterSpacing: "0.05em", textTransform: "uppercase" }}>
|
| 102 |
-
Assistant
|
| 103 |
-
</Typography>
|
| 104 |
-
<Tooltip title="New conversation" arrow>
|
| 105 |
-
<IconButton size="small" onClick={onNewChat} sx={{ color: "text.disabled" }}>
|
| 106 |
-
<AddIcon sx={{ fontSize: 16 }} />
|
| 107 |
-
</IconButton>
|
| 108 |
</Tooltip>
|
| 109 |
-
</
|
| 110 |
|
| 111 |
{/* Messages */}
|
| 112 |
-
<
|
| 113 |
-
ref={scrollRef}
|
| 114 |
-
sx={{
|
| 115 |
-
flex: 1,
|
| 116 |
-
overflow: "auto",
|
| 117 |
-
px: 1.5,
|
| 118 |
-
py: 1,
|
| 119 |
-
display: "flex",
|
| 120 |
-
flexDirection: "column",
|
| 121 |
-
gap: 1.5,
|
| 122 |
-
}}
|
| 123 |
-
>
|
| 124 |
{messages.length === 0 && (
|
| 125 |
-
<
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
</Typography>
|
| 129 |
-
</Box>
|
| 130 |
)}
|
| 131 |
|
| 132 |
{messages.map((msg) => (
|
|
@@ -134,106 +95,65 @@ export function ChatPanel({
|
|
| 134 |
))}
|
| 135 |
|
| 136 |
{isLoading && messages.length > 0 && !messages[messages.length - 1]?.content && (
|
| 137 |
-
<
|
| 138 |
-
<
|
| 139 |
-
<
|
| 140 |
-
|
| 141 |
-
</Typography>
|
| 142 |
-
</Box>
|
| 143 |
)}
|
| 144 |
|
| 145 |
{error && (
|
| 146 |
-
<
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
</Typography>
|
| 150 |
-
</Box>
|
| 151 |
)}
|
| 152 |
-
</
|
| 153 |
|
| 154 |
{/* Quick actions */}
|
| 155 |
{hasSelection && (
|
| 156 |
-
<
|
| 157 |
-
sx={{
|
| 158 |
-
display: "flex",
|
| 159 |
-
flexWrap: "wrap",
|
| 160 |
-
gap: 0.5,
|
| 161 |
-
px: 1.5,
|
| 162 |
-
py: 0.75,
|
| 163 |
-
borderTop: "1px solid",
|
| 164 |
-
borderColor: "divider",
|
| 165 |
-
flexShrink: 0,
|
| 166 |
-
}}
|
| 167 |
-
>
|
| 168 |
{QUICK_ACTIONS.map((action) => (
|
| 169 |
-
<
|
| 170 |
key={action.id}
|
| 171 |
-
|
| 172 |
-
icon={action.icon}
|
| 173 |
-
size="small"
|
| 174 |
-
variant="outlined"
|
| 175 |
onClick={() => onQuickAction(action.id)}
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
"&:hover": { borderColor: "text.secondary", bgcolor: "action.hover" },
|
| 182 |
-
}}
|
| 183 |
-
/>
|
| 184 |
))}
|
| 185 |
-
</
|
| 186 |
)}
|
| 187 |
|
| 188 |
{/* Input */}
|
| 189 |
-
<
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
borderColor: "divider",
|
| 195 |
-
flexShrink: 0,
|
| 196 |
-
}}
|
| 197 |
-
>
|
| 198 |
-
<Box sx={{ display: "flex", gap: 0.5, alignItems: "flex-end" }}>
|
| 199 |
-
<TextField
|
| 200 |
-
multiline
|
| 201 |
-
maxRows={4}
|
| 202 |
-
fullWidth
|
| 203 |
-
size="small"
|
| 204 |
placeholder="Ask anything..."
|
| 205 |
value={inputValue}
|
| 206 |
onChange={(e) => onSetInput(e.target.value)}
|
| 207 |
onKeyDown={handleKeyDown}
|
| 208 |
-
variant="standard"
|
| 209 |
-
slotProps={{
|
| 210 |
-
input: {
|
| 211 |
-
disableUnderline: true,
|
| 212 |
-
sx: {
|
| 213 |
-
fontSize: "0.85rem",
|
| 214 |
-
lineHeight: 1.5,
|
| 215 |
-
py: 0.5,
|
| 216 |
-
},
|
| 217 |
-
},
|
| 218 |
-
}}
|
| 219 |
/>
|
| 220 |
{isLoading ? (
|
| 221 |
-
<
|
| 222 |
-
<
|
| 223 |
-
</
|
| 224 |
) : (
|
| 225 |
-
<
|
| 226 |
-
|
| 227 |
onClick={handleSubmit}
|
| 228 |
disabled={!inputValue.trim()}
|
| 229 |
-
|
|
|
|
| 230 |
>
|
| 231 |
-
<
|
| 232 |
-
</
|
| 233 |
)}
|
| 234 |
-
</
|
| 235 |
-
</
|
| 236 |
-
</
|
| 237 |
);
|
| 238 |
}
|
| 239 |
|
|
@@ -250,59 +170,30 @@ function MessageBubble({ message }: { message: UIMessage }) {
|
|
| 250 |
if (!textContent && message.role === "assistant" && toolParts.length === 0) return null;
|
| 251 |
|
| 252 |
return (
|
| 253 |
-
<
|
| 254 |
{textContent && (
|
| 255 |
-
<
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
maxWidth: "95%",
|
| 259 |
-
px: 1.5,
|
| 260 |
-
py: 0.75,
|
| 261 |
-
borderRadius: 2,
|
| 262 |
-
bgcolor: isUser ? "rgba(149, 141, 241, 0.15)" : "transparent",
|
| 263 |
-
}}
|
| 264 |
-
>
|
| 265 |
-
<Typography
|
| 266 |
-
variant="body2"
|
| 267 |
-
sx={{
|
| 268 |
-
fontSize: "0.8rem",
|
| 269 |
-
lineHeight: 1.6,
|
| 270 |
-
color: isUser ? "text.primary" : "text.secondary",
|
| 271 |
-
whiteSpace: "pre-wrap",
|
| 272 |
-
wordBreak: "break-word",
|
| 273 |
-
}}
|
| 274 |
-
>
|
| 275 |
-
{textContent}
|
| 276 |
-
</Typography>
|
| 277 |
-
</Box>
|
| 278 |
)}
|
| 279 |
|
| 280 |
{toolParts.map((part, i) => {
|
| 281 |
const tool = part as { type: "tool-invocation"; toolInvocation: { toolName: string; state: string; result?: unknown } };
|
| 282 |
return (
|
| 283 |
-
<
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
display: "flex",
|
| 287 |
-
alignItems: "center",
|
| 288 |
-
gap: 0.5,
|
| 289 |
-
px: 1.5,
|
| 290 |
-
opacity: 0.6,
|
| 291 |
-
}}
|
| 292 |
-
>
|
| 293 |
-
<AutoFixHighIcon sx={{ fontSize: 12 }} />
|
| 294 |
-
<Typography variant="caption" sx={{ fontSize: "0.65rem" }}>
|
| 295 |
{toolLabel(tool.toolInvocation.toolName)}
|
| 296 |
{tool.toolInvocation.state === "result" && tool.toolInvocation.result
|
| 297 |
? ` - ${tool.toolInvocation.result}`
|
| 298 |
: tool.toolInvocation.state === "call"
|
| 299 |
? " (executing...)"
|
| 300 |
: ""}
|
| 301 |
-
</
|
| 302 |
-
</
|
| 303 |
);
|
| 304 |
})}
|
| 305 |
-
</
|
| 306 |
);
|
| 307 |
}
|
| 308 |
|
|
|
|
| 1 |
import { useState, useRef, useEffect, type KeyboardEvent } from "react";
|
| 2 |
+
import { Tooltip } from "./Tooltip";
|
| 3 |
import {
|
| 4 |
+
Send,
|
| 5 |
+
Square,
|
| 6 |
+
Plus,
|
| 7 |
+
Sparkles,
|
| 8 |
+
AlignLeft,
|
| 9 |
+
Expand,
|
| 10 |
+
SpellCheck,
|
| 11 |
+
Languages,
|
| 12 |
+
Shrink,
|
| 13 |
+
} from "lucide-react";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
import type { UIMessage } from "ai";
|
| 15 |
|
| 16 |
interface ChatPanelProps {
|
|
|
|
| 27 |
}
|
| 28 |
|
| 29 |
const QUICK_ACTIONS = [
|
| 30 |
+
{ id: "rewrite", label: "Rewrite", Icon: Sparkles },
|
| 31 |
+
{ id: "expand", label: "Expand", Icon: Expand },
|
| 32 |
+
{ id: "summarize", label: "Summarize", Icon: Shrink },
|
| 33 |
+
{ id: "fix-grammar", label: "Fix grammar", Icon: SpellCheck },
|
| 34 |
+
{ id: "translate", label: "Translate", Icon: Languages },
|
| 35 |
+
{ id: "simplify", label: "Simplify", Icon: AlignLeft },
|
| 36 |
];
|
| 37 |
|
| 38 |
export function ChatPanel({
|
|
|
|
| 71 |
};
|
| 72 |
|
| 73 |
return (
|
| 74 |
+
<div className="chat-panel">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
{/* Header */}
|
| 76 |
+
<div className="chat-panel__header">
|
| 77 |
+
<span className="chat-panel__label">Assistant</span>
|
| 78 |
+
<Tooltip title="New conversation">
|
| 79 |
+
<button className="icon-btn" onClick={onNewChat} aria-label="New conversation">
|
| 80 |
+
<Plus size={16} />
|
| 81 |
+
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
</Tooltip>
|
| 83 |
+
</div>
|
| 84 |
|
| 85 |
{/* Messages */}
|
| 86 |
+
<div ref={scrollRef} className="chat-panel__messages">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
{messages.length === 0 && (
|
| 88 |
+
<div className="chat-panel__empty">
|
| 89 |
+
Ask me to write, edit, expand, or improve your article.
|
| 90 |
+
</div>
|
|
|
|
|
|
|
| 91 |
)}
|
| 92 |
|
| 93 |
{messages.map((msg) => (
|
|
|
|
| 95 |
))}
|
| 96 |
|
| 97 |
{isLoading && messages.length > 0 && !messages[messages.length - 1]?.content && (
|
| 98 |
+
<div className="chat-panel__thinking">
|
| 99 |
+
<span className="spinner" style={{ width: 12, height: 12, borderWidth: 2 }} />
|
| 100 |
+
<span>Thinking...</span>
|
| 101 |
+
</div>
|
|
|
|
|
|
|
| 102 |
)}
|
| 103 |
|
| 104 |
{error && (
|
| 105 |
+
<div className="chat-panel__error">
|
| 106 |
+
{error.message}
|
| 107 |
+
</div>
|
|
|
|
|
|
|
| 108 |
)}
|
| 109 |
+
</div>
|
| 110 |
|
| 111 |
{/* Quick actions */}
|
| 112 |
{hasSelection && (
|
| 113 |
+
<div className="chat-panel__actions">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
{QUICK_ACTIONS.map((action) => (
|
| 115 |
+
<button
|
| 116 |
key={action.id}
|
| 117 |
+
className="chip chip--outlined chip--clickable"
|
|
|
|
|
|
|
|
|
|
| 118 |
onClick={() => onQuickAction(action.id)}
|
| 119 |
+
style={{ height: 24, fontSize: "0.65rem", gap: 3 }}
|
| 120 |
+
>
|
| 121 |
+
<action.Icon size={12} />
|
| 122 |
+
{action.label}
|
| 123 |
+
</button>
|
|
|
|
|
|
|
|
|
|
| 124 |
))}
|
| 125 |
+
</div>
|
| 126 |
)}
|
| 127 |
|
| 128 |
{/* Input */}
|
| 129 |
+
<div className="chat-panel__input">
|
| 130 |
+
<div className="chat-panel__input-row">
|
| 131 |
+
<textarea
|
| 132 |
+
className="chat-panel__textarea"
|
| 133 |
+
rows={1}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
placeholder="Ask anything..."
|
| 135 |
value={inputValue}
|
| 136 |
onChange={(e) => onSetInput(e.target.value)}
|
| 137 |
onKeyDown={handleKeyDown}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
/>
|
| 139 |
{isLoading ? (
|
| 140 |
+
<button className="icon-btn" onClick={onStop} aria-label="Stop" style={{ color: "#ff9800", flexShrink: 0 }}>
|
| 141 |
+
<Square size={18} />
|
| 142 |
+
</button>
|
| 143 |
) : (
|
| 144 |
+
<button
|
| 145 |
+
className="icon-btn"
|
| 146 |
onClick={handleSubmit}
|
| 147 |
disabled={!inputValue.trim()}
|
| 148 |
+
aria-label="Send"
|
| 149 |
+
style={{ color: inputValue.trim() ? "var(--primary-color)" : "var(--ed-text-disabled)", flexShrink: 0 }}
|
| 150 |
>
|
| 151 |
+
<Send size={18} />
|
| 152 |
+
</button>
|
| 153 |
)}
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
);
|
| 158 |
}
|
| 159 |
|
|
|
|
| 170 |
if (!textContent && message.role === "assistant" && toolParts.length === 0) return null;
|
| 171 |
|
| 172 |
return (
|
| 173 |
+
<div className="chat-bubble">
|
| 174 |
{textContent && (
|
| 175 |
+
<div className={`chat-bubble__text ${isUser ? "chat-bubble__text--user" : ""}`}>
|
| 176 |
+
{textContent}
|
| 177 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
)}
|
| 179 |
|
| 180 |
{toolParts.map((part, i) => {
|
| 181 |
const tool = part as { type: "tool-invocation"; toolInvocation: { toolName: string; state: string; result?: unknown } };
|
| 182 |
return (
|
| 183 |
+
<div key={i} className="chat-bubble__tool">
|
| 184 |
+
<Sparkles size={12} />
|
| 185 |
+
<span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
{toolLabel(tool.toolInvocation.toolName)}
|
| 187 |
{tool.toolInvocation.state === "result" && tool.toolInvocation.result
|
| 188 |
? ` - ${tool.toolInvocation.result}`
|
| 189 |
: tool.toolInvocation.state === "call"
|
| 190 |
? " (executing...)"
|
| 191 |
: ""}
|
| 192 |
+
</span>
|
| 193 |
+
</div>
|
| 194 |
);
|
| 195 |
})}
|
| 196 |
+
</div>
|
| 197 |
);
|
| 198 |
}
|
| 199 |
|
|
@@ -1,11 +1,4 @@
|
|
| 1 |
-
import { useState, useEffect, useRef } from "react";
|
| 2 |
-
import {
|
| 3 |
-
Dialog,
|
| 4 |
-
DialogContent,
|
| 5 |
-
TextField,
|
| 6 |
-
Button,
|
| 7 |
-
Box,
|
| 8 |
-
} from "@mui/material";
|
| 9 |
|
| 10 |
interface CommentDialogProps {
|
| 11 |
open: boolean;
|
|
@@ -15,66 +8,70 @@ interface CommentDialogProps {
|
|
| 15 |
|
| 16 |
export function CommentDialog({ open, onClose, onSubmit }: CommentDialogProps) {
|
| 17 |
const [text, setText] = useState("");
|
| 18 |
-
const
|
|
|
|
| 19 |
|
| 20 |
useEffect(() => {
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
setText("");
|
| 23 |
-
setTimeout(() => inputRef.current?.focus(),
|
|
|
|
|
|
|
| 24 |
}
|
| 25 |
}, [open]);
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
const handleSubmit = () => {
|
| 28 |
if (!text.trim()) return;
|
| 29 |
onSubmit(text.trim());
|
| 30 |
setText("");
|
| 31 |
-
|
| 32 |
};
|
| 33 |
|
| 34 |
return (
|
| 35 |
-
<
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
sx: {
|
| 42 |
-
bgcolor: "background.paper",
|
| 43 |
-
backgroundImage: "none",
|
| 44 |
-
},
|
| 45 |
-
}}
|
| 46 |
-
>
|
| 47 |
-
<DialogContent sx={{ p: 2 }}>
|
| 48 |
-
<TextField
|
| 49 |
-
inputRef={inputRef}
|
| 50 |
-
multiline
|
| 51 |
-
minRows={2}
|
| 52 |
-
maxRows={4}
|
| 53 |
-
fullWidth
|
| 54 |
-
size="small"
|
| 55 |
placeholder="Add your comment..."
|
| 56 |
value={text}
|
| 57 |
onChange={(e) => setText(e.target.value)}
|
| 58 |
onKeyDown={(e) => {
|
| 59 |
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleSubmit();
|
| 60 |
}}
|
| 61 |
-
|
| 62 |
/>
|
| 63 |
-
<
|
| 64 |
-
<
|
| 65 |
Cancel
|
| 66 |
-
</
|
| 67 |
-
<
|
| 68 |
-
|
| 69 |
-
variant="contained"
|
| 70 |
onClick={handleSubmit}
|
| 71 |
disabled={!text.trim()}
|
| 72 |
-
sx={{ textTransform: "none" }}
|
| 73 |
>
|
| 74 |
Comment
|
| 75 |
-
</
|
| 76 |
-
</
|
| 77 |
-
</
|
| 78 |
-
</
|
| 79 |
);
|
| 80 |
}
|
|
|
|
| 1 |
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
interface CommentDialogProps {
|
| 4 |
open: boolean;
|
|
|
|
| 8 |
|
| 9 |
export function CommentDialog({ open, onClose, onSubmit }: CommentDialogProps) {
|
| 10 |
const [text, setText] = useState("");
|
| 11 |
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
| 12 |
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
| 13 |
|
| 14 |
useEffect(() => {
|
| 15 |
+
const dialog = dialogRef.current;
|
| 16 |
+
if (!dialog) return;
|
| 17 |
+
|
| 18 |
+
if (open && !dialog.open) {
|
| 19 |
+
dialog.showModal();
|
| 20 |
setText("");
|
| 21 |
+
setTimeout(() => inputRef.current?.focus(), 50);
|
| 22 |
+
} else if (!open && dialog.open) {
|
| 23 |
+
dialog.close();
|
| 24 |
}
|
| 25 |
}, [open]);
|
| 26 |
|
| 27 |
+
const handleClose = useCallback(() => {
|
| 28 |
+
dialogRef.current?.close();
|
| 29 |
+
onClose();
|
| 30 |
+
}, [onClose]);
|
| 31 |
+
|
| 32 |
+
useEffect(() => {
|
| 33 |
+
const dialog = dialogRef.current;
|
| 34 |
+
if (!dialog) return;
|
| 35 |
+
const onCancel = () => onClose();
|
| 36 |
+
dialog.addEventListener("close", onCancel);
|
| 37 |
+
return () => dialog.removeEventListener("close", onCancel);
|
| 38 |
+
}, [onClose]);
|
| 39 |
+
|
| 40 |
const handleSubmit = () => {
|
| 41 |
if (!text.trim()) return;
|
| 42 |
onSubmit(text.trim());
|
| 43 |
setText("");
|
| 44 |
+
handleClose();
|
| 45 |
};
|
| 46 |
|
| 47 |
return (
|
| 48 |
+
<dialog ref={dialogRef} className="ed-dialog">
|
| 49 |
+
<div className="ed-dialog__body" style={{ padding: "16px" }}>
|
| 50 |
+
<textarea
|
| 51 |
+
ref={inputRef}
|
| 52 |
+
className="form-input"
|
| 53 |
+
rows={3}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
placeholder="Add your comment..."
|
| 55 |
value={text}
|
| 56 |
onChange={(e) => setText(e.target.value)}
|
| 57 |
onKeyDown={(e) => {
|
| 58 |
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleSubmit();
|
| 59 |
}}
|
| 60 |
+
style={{ marginBottom: 12 }}
|
| 61 |
/>
|
| 62 |
+
<div className="ed-dialog__actions" style={{ padding: 0 }}>
|
| 63 |
+
<button className="btn" onClick={handleClose}>
|
| 64 |
Cancel
|
| 65 |
+
</button>
|
| 66 |
+
<button
|
| 67 |
+
className="btn btn--primary"
|
|
|
|
| 68 |
onClick={handleSubmit}
|
| 69 |
disabled={!text.trim()}
|
|
|
|
| 70 |
>
|
| 71 |
Comment
|
| 72 |
+
</button>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
</dialog>
|
| 76 |
);
|
| 77 |
}
|
|
@@ -1,15 +1,6 @@
|
|
| 1 |
import { useState, useEffect, useCallback, useRef } from "react";
|
| 2 |
-
import {
|
| 3 |
-
|
| 4 |
-
Typography,
|
| 5 |
-
IconButton,
|
| 6 |
-
Chip,
|
| 7 |
-
Paper,
|
| 8 |
-
Tooltip,
|
| 9 |
-
Fade,
|
| 10 |
-
} from "@mui/material";
|
| 11 |
-
import CheckCircleOutlinedIcon from "@mui/icons-material/CheckCircleOutlined";
|
| 12 |
-
import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined";
|
| 13 |
import type { Editor } from "@tiptap/core";
|
| 14 |
import type { CommentStore, CommentData } from "../editor/comments";
|
| 15 |
|
|
@@ -41,7 +32,6 @@ export function CommentsSidebar({ editor, commentStore, user, editorContainer }:
|
|
| 41 |
return commentStore.observe(refresh);
|
| 42 |
}, [commentStore, refresh]);
|
| 43 |
|
| 44 |
-
// Compute Y positions of comment marks in the editor
|
| 45 |
const updatePositions = useCallback(() => {
|
| 46 |
if (!editor || !editorContainer) return;
|
| 47 |
|
|
@@ -74,7 +64,6 @@ export function CommentsSidebar({ editor, commentStore, user, editorContainer }:
|
|
| 74 |
}
|
| 75 |
}
|
| 76 |
|
| 77 |
-
// Avoid overlaps: push comments down if they'd overlap the previous one
|
| 78 |
result.sort((a, b) => a.top - b.top);
|
| 79 |
const MIN_GAP = 80;
|
| 80 |
for (let i = 1; i < result.length; i++) {
|
|
@@ -118,11 +107,6 @@ export function CommentsSidebar({ editor, commentStore, user, editorContainer }:
|
|
| 118 |
commentStore.resolve(id, user.name);
|
| 119 |
};
|
| 120 |
|
| 121 |
-
const handleUnresolve = (id: string) => {
|
| 122 |
-
if (!commentStore) return;
|
| 123 |
-
commentStore.unresolve(id);
|
| 124 |
-
};
|
| 125 |
-
|
| 126 |
const handleDelete = (id: string) => {
|
| 127 |
if (!commentStore || !editor) return;
|
| 128 |
commentStore.remove(id);
|
|
@@ -140,30 +124,28 @@ export function CommentsSidebar({ editor, commentStore, user, editorContainer }:
|
|
| 140 |
};
|
| 141 |
|
| 142 |
return (
|
| 143 |
-
<
|
| 144 |
-
{/* Positioned comments */}
|
| 145 |
{positioned.map((c) => (
|
| 146 |
-
<
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
</Fade>
|
| 165 |
))}
|
| 166 |
-
</
|
| 167 |
);
|
| 168 |
}
|
| 169 |
|
|
@@ -188,53 +170,43 @@ function CommentCard({
|
|
| 188 |
});
|
| 189 |
|
| 190 |
return (
|
| 191 |
-
<
|
| 192 |
-
|
|
|
|
| 193 |
onClick={onClick}
|
| 194 |
-
sx={{
|
| 195 |
-
p: 1.25,
|
| 196 |
-
cursor: "pointer",
|
| 197 |
-
borderLeft: 3,
|
| 198 |
-
borderLeftColor: comment.authorColor,
|
| 199 |
-
borderColor: active ? "primary.main" : "divider",
|
| 200 |
-
bgcolor: active ? "rgba(149,141,241,0.05)" : "background.paper",
|
| 201 |
-
transition: "all 0.15s",
|
| 202 |
-
"&:hover": { borderColor: "primary.main" },
|
| 203 |
-
}}
|
| 204 |
>
|
| 205 |
-
<
|
| 206 |
-
<
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
<Box sx={{ display: "flex", gap: 0.25 }}>
|
| 227 |
-
<Tooltip title="Resolve" arrow>
|
| 228 |
-
<IconButton size="small" onClick={(e) => { e.stopPropagation(); onResolve(); }} sx={{ color: "success.main", p: 0.5 }}>
|
| 229 |
-
<CheckCircleOutlinedIcon sx={{ fontSize: 16 }} />
|
| 230 |
-
</IconButton>
|
| 231 |
</Tooltip>
|
| 232 |
-
<Tooltip title="Delete"
|
| 233 |
-
<
|
| 234 |
-
|
| 235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
</Tooltip>
|
| 237 |
-
</
|
| 238 |
-
</
|
| 239 |
);
|
| 240 |
}
|
|
|
|
| 1 |
import { useState, useEffect, useCallback, useRef } from "react";
|
| 2 |
+
import { Tooltip } from "./Tooltip";
|
| 3 |
+
import { CheckCircle, Trash2 } from "lucide-react";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import type { Editor } from "@tiptap/core";
|
| 5 |
import type { CommentStore, CommentData } from "../editor/comments";
|
| 6 |
|
|
|
|
| 32 |
return commentStore.observe(refresh);
|
| 33 |
}, [commentStore, refresh]);
|
| 34 |
|
|
|
|
| 35 |
const updatePositions = useCallback(() => {
|
| 36 |
if (!editor || !editorContainer) return;
|
| 37 |
|
|
|
|
| 64 |
}
|
| 65 |
}
|
| 66 |
|
|
|
|
| 67 |
result.sort((a, b) => a.top - b.top);
|
| 68 |
const MIN_GAP = 80;
|
| 69 |
for (let i = 1; i < result.length; i++) {
|
|
|
|
| 107 |
commentStore.resolve(id, user.name);
|
| 108 |
};
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
const handleDelete = (id: string) => {
|
| 111 |
if (!commentStore || !editor) return;
|
| 112 |
commentStore.remove(id);
|
|
|
|
| 124 |
};
|
| 125 |
|
| 126 |
return (
|
| 127 |
+
<div ref={sidebarRef} style={{ position: "relative", width: "100%", minHeight: "100%" }}>
|
|
|
|
| 128 |
{positioned.map((c) => (
|
| 129 |
+
<div
|
| 130 |
+
key={c.id}
|
| 131 |
+
style={{
|
| 132 |
+
position: "absolute",
|
| 133 |
+
top: c.top,
|
| 134 |
+
left: 0,
|
| 135 |
+
right: 0,
|
| 136 |
+
transition: "top 0.2s ease",
|
| 137 |
+
}}
|
| 138 |
+
>
|
| 139 |
+
<CommentCard
|
| 140 |
+
comment={c}
|
| 141 |
+
active={activeId === c.id}
|
| 142 |
+
onResolve={() => handleResolve(c.id)}
|
| 143 |
+
onDelete={() => handleDelete(c.id)}
|
| 144 |
+
onClick={() => setActiveId(activeId === c.id ? null : c.id)}
|
| 145 |
+
/>
|
| 146 |
+
</div>
|
|
|
|
| 147 |
))}
|
| 148 |
+
</div>
|
| 149 |
);
|
| 150 |
}
|
| 151 |
|
|
|
|
| 170 |
});
|
| 171 |
|
| 172 |
return (
|
| 173 |
+
<div
|
| 174 |
+
className={`comment-card surface ${active ? "comment-card--active" : ""}`}
|
| 175 |
+
style={{ borderLeftColor: comment.authorColor }}
|
| 176 |
onClick={onClick}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
>
|
| 178 |
+
<div className="comment-card__header">
|
| 179 |
+
<span
|
| 180 |
+
className="chip"
|
| 181 |
+
style={{ backgroundColor: comment.authorColor, color: "#000" }}
|
| 182 |
+
>
|
| 183 |
+
{comment.author}
|
| 184 |
+
</span>
|
| 185 |
+
<span className="comment-card__time">{time}</span>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
<div className="comment-card__text">{comment.text}</div>
|
| 189 |
+
|
| 190 |
+
<div className="comment-card__actions">
|
| 191 |
+
<Tooltip title="Resolve">
|
| 192 |
+
<button
|
| 193 |
+
className="icon-btn icon-btn--success"
|
| 194 |
+
onClick={(e) => { e.stopPropagation(); onResolve(); }}
|
| 195 |
+
aria-label="Resolve"
|
| 196 |
+
>
|
| 197 |
+
<CheckCircle size={16} />
|
| 198 |
+
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
</Tooltip>
|
| 200 |
+
<Tooltip title="Delete">
|
| 201 |
+
<button
|
| 202 |
+
className="icon-btn icon-btn--danger"
|
| 203 |
+
onClick={(e) => { e.stopPropagation(); onDelete(); }}
|
| 204 |
+
aria-label="Delete"
|
| 205 |
+
>
|
| 206 |
+
<Trash2 size={16} />
|
| 207 |
+
</button>
|
| 208 |
</Tooltip>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
);
|
| 212 |
}
|
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef, useCallback, useEffect, type ReactNode } from "react";
|
| 2 |
+
import { computePosition, offset, flip, shift, type Placement } from "@floating-ui/dom";
|
| 3 |
+
|
| 4 |
+
interface TooltipProps {
|
| 5 |
+
title: string;
|
| 6 |
+
placement?: Placement;
|
| 7 |
+
children: ReactNode;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export function Tooltip({ title, placement = "top", children }: TooltipProps) {
|
| 11 |
+
const triggerRef = useRef<HTMLSpanElement>(null);
|
| 12 |
+
const tooltipRef = useRef<HTMLDivElement>(null);
|
| 13 |
+
const [open, setOpen] = useState(false);
|
| 14 |
+
|
| 15 |
+
const reposition = useCallback(() => {
|
| 16 |
+
const trigger = triggerRef.current;
|
| 17 |
+
const tip = tooltipRef.current;
|
| 18 |
+
if (!trigger || !tip) return;
|
| 19 |
+
|
| 20 |
+
computePosition(trigger, tip, {
|
| 21 |
+
placement,
|
| 22 |
+
strategy: "fixed",
|
| 23 |
+
middleware: [offset(6), flip(), shift({ padding: 8 })],
|
| 24 |
+
}).then(({ x, y }) => {
|
| 25 |
+
tip.style.left = `${x}px`;
|
| 26 |
+
tip.style.top = `${y}px`;
|
| 27 |
+
});
|
| 28 |
+
}, [placement]);
|
| 29 |
+
|
| 30 |
+
useEffect(() => {
|
| 31 |
+
if (open) reposition();
|
| 32 |
+
}, [open, reposition]);
|
| 33 |
+
|
| 34 |
+
if (!title) return <>{children}</>;
|
| 35 |
+
|
| 36 |
+
return (
|
| 37 |
+
<>
|
| 38 |
+
<span
|
| 39 |
+
ref={triggerRef}
|
| 40 |
+
onMouseEnter={() => setOpen(true)}
|
| 41 |
+
onMouseLeave={() => setOpen(false)}
|
| 42 |
+
onFocus={() => setOpen(true)}
|
| 43 |
+
onBlur={() => setOpen(false)}
|
| 44 |
+
style={{ display: "inline-flex" }}
|
| 45 |
+
>
|
| 46 |
+
{children}
|
| 47 |
+
</span>
|
| 48 |
+
{open && (
|
| 49 |
+
<div ref={tooltipRef} className="ed-tooltip" role="tooltip">
|
| 50 |
+
{title}
|
| 51 |
+
</div>
|
| 52 |
+
)}
|
| 53 |
+
</>
|
| 54 |
+
);
|
| 55 |
+
}
|
|
@@ -1,15 +1,17 @@
|
|
| 1 |
import { BubbleMenu } from "@tiptap/react/menus";
|
| 2 |
import type { Editor } from "@tiptap/core";
|
| 3 |
-
import {
|
| 4 |
-
import
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
| 13 |
|
| 14 |
interface BubbleToolbarProps {
|
| 15 |
editor: Editor;
|
|
@@ -28,22 +30,17 @@ function Btn({
|
|
| 28 |
children: React.ReactNode;
|
| 29 |
}) {
|
| 30 |
return (
|
| 31 |
-
<Tooltip title={tooltip}
|
| 32 |
-
<
|
|
|
|
| 33 |
onMouseDown={(e) => {
|
| 34 |
e.preventDefault();
|
| 35 |
onClick();
|
| 36 |
}}
|
| 37 |
-
|
| 38 |
-
sx={{
|
| 39 |
-
color: active ? "#fff" : "rgba(255,255,255,0.6)",
|
| 40 |
-
"&:hover": { color: "#fff" },
|
| 41 |
-
borderRadius: 1,
|
| 42 |
-
p: 0.75,
|
| 43 |
-
}}
|
| 44 |
>
|
| 45 |
{children}
|
| 46 |
-
</
|
| 47 |
</Tooltip>
|
| 48 |
);
|
| 49 |
}
|
|
@@ -61,91 +58,76 @@ export function BubbleToolbar({ editor, onAddComment }: BubbleToolbarProps) {
|
|
| 61 |
editor={editor}
|
| 62 |
options={{ placement: "top", offset: 6 }}
|
| 63 |
>
|
| 64 |
-
<
|
| 65 |
-
sx={{
|
| 66 |
-
display: "flex",
|
| 67 |
-
alignItems: "center",
|
| 68 |
-
gap: 0.25,
|
| 69 |
-
bgcolor: "#1a1a1a",
|
| 70 |
-
border: "1px solid #333",
|
| 71 |
-
borderRadius: 2,
|
| 72 |
-
px: 0.5,
|
| 73 |
-
py: 0.25,
|
| 74 |
-
boxShadow: "0 4px 20px rgba(0,0,0,0.4)",
|
| 75 |
-
}}
|
| 76 |
-
>
|
| 77 |
<Btn
|
| 78 |
onClick={() => editor.chain().focus().toggleBold().run()}
|
| 79 |
active={editor.isActive("bold")}
|
| 80 |
tooltip="Bold"
|
| 81 |
>
|
| 82 |
-
<
|
| 83 |
</Btn>
|
| 84 |
<Btn
|
| 85 |
onClick={() => editor.chain().focus().toggleItalic().run()}
|
| 86 |
active={editor.isActive("italic")}
|
| 87 |
tooltip="Italic"
|
| 88 |
>
|
| 89 |
-
<
|
| 90 |
</Btn>
|
| 91 |
<Btn
|
| 92 |
onClick={() => editor.chain().focus().toggleStrike().run()}
|
| 93 |
active={editor.isActive("strike")}
|
| 94 |
tooltip="Strikethrough"
|
| 95 |
>
|
| 96 |
-
<
|
| 97 |
</Btn>
|
| 98 |
<Btn
|
| 99 |
onClick={() => editor.chain().focus().toggleCode().run()}
|
| 100 |
active={editor.isActive("code")}
|
| 101 |
tooltip="Code"
|
| 102 |
>
|
| 103 |
-
<
|
| 104 |
</Btn>
|
| 105 |
<Btn
|
| 106 |
onClick={() => editor.chain().focus().insertInlineMath({ latex: "x^2" }).run()}
|
| 107 |
active={editor.isActive("inlineMath")}
|
| 108 |
tooltip="Inline math"
|
| 109 |
>
|
| 110 |
-
<
|
| 111 |
</Btn>
|
| 112 |
|
| 113 |
-
<
|
| 114 |
|
| 115 |
<Btn
|
| 116 |
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
| 117 |
active={editor.isActive("heading", { level: 2 })}
|
| 118 |
tooltip="Heading"
|
| 119 |
>
|
| 120 |
-
<
|
| 121 |
</Btn>
|
| 122 |
<Btn
|
| 123 |
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
| 124 |
active={editor.isActive("blockquote")}
|
| 125 |
tooltip="Quote"
|
| 126 |
>
|
| 127 |
-
<
|
| 128 |
</Btn>
|
| 129 |
<Btn
|
| 130 |
onClick={setLink}
|
| 131 |
active={editor.isActive("link")}
|
| 132 |
tooltip="Link"
|
| 133 |
>
|
| 134 |
-
<
|
| 135 |
</Btn>
|
| 136 |
|
| 137 |
{onAddComment && (
|
| 138 |
<>
|
| 139 |
-
<
|
| 140 |
-
<Btn
|
| 141 |
-
|
| 142 |
-
tooltip="Comment"
|
| 143 |
-
>
|
| 144 |
-
<ChatBubbleOutlinedIcon sx={{ fontSize: 18 }} />
|
| 145 |
</Btn>
|
| 146 |
</>
|
| 147 |
)}
|
| 148 |
-
</
|
| 149 |
</BubbleMenu>
|
| 150 |
);
|
| 151 |
}
|
|
|
|
| 1 |
import { BubbleMenu } from "@tiptap/react/menus";
|
| 2 |
import type { Editor } from "@tiptap/core";
|
| 3 |
+
import { Tooltip } from "../components/Tooltip";
|
| 4 |
+
import {
|
| 5 |
+
Bold,
|
| 6 |
+
Italic,
|
| 7 |
+
Strikethrough,
|
| 8 |
+
Code,
|
| 9 |
+
Quote,
|
| 10 |
+
Link,
|
| 11 |
+
Heading2,
|
| 12 |
+
MessageCircle,
|
| 13 |
+
Sigma,
|
| 14 |
+
} from "lucide-react";
|
| 15 |
|
| 16 |
interface BubbleToolbarProps {
|
| 17 |
editor: Editor;
|
|
|
|
| 30 |
children: React.ReactNode;
|
| 31 |
}) {
|
| 32 |
return (
|
| 33 |
+
<Tooltip title={tooltip} placement="top">
|
| 34 |
+
<button
|
| 35 |
+
className={`icon-btn ${active ? "icon-btn--active" : ""}`}
|
| 36 |
onMouseDown={(e) => {
|
| 37 |
e.preventDefault();
|
| 38 |
onClick();
|
| 39 |
}}
|
| 40 |
+
aria-label={tooltip}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
>
|
| 42 |
{children}
|
| 43 |
+
</button>
|
| 44 |
</Tooltip>
|
| 45 |
);
|
| 46 |
}
|
|
|
|
| 58 |
editor={editor}
|
| 59 |
options={{ placement: "top", offset: 6 }}
|
| 60 |
>
|
| 61 |
+
<div className="bubble-toolbar">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
<Btn
|
| 63 |
onClick={() => editor.chain().focus().toggleBold().run()}
|
| 64 |
active={editor.isActive("bold")}
|
| 65 |
tooltip="Bold"
|
| 66 |
>
|
| 67 |
+
<Bold size={18} />
|
| 68 |
</Btn>
|
| 69 |
<Btn
|
| 70 |
onClick={() => editor.chain().focus().toggleItalic().run()}
|
| 71 |
active={editor.isActive("italic")}
|
| 72 |
tooltip="Italic"
|
| 73 |
>
|
| 74 |
+
<Italic size={18} />
|
| 75 |
</Btn>
|
| 76 |
<Btn
|
| 77 |
onClick={() => editor.chain().focus().toggleStrike().run()}
|
| 78 |
active={editor.isActive("strike")}
|
| 79 |
tooltip="Strikethrough"
|
| 80 |
>
|
| 81 |
+
<Strikethrough size={18} />
|
| 82 |
</Btn>
|
| 83 |
<Btn
|
| 84 |
onClick={() => editor.chain().focus().toggleCode().run()}
|
| 85 |
active={editor.isActive("code")}
|
| 86 |
tooltip="Code"
|
| 87 |
>
|
| 88 |
+
<Code size={18} />
|
| 89 |
</Btn>
|
| 90 |
<Btn
|
| 91 |
onClick={() => editor.chain().focus().insertInlineMath({ latex: "x^2" }).run()}
|
| 92 |
active={editor.isActive("inlineMath")}
|
| 93 |
tooltip="Inline math"
|
| 94 |
>
|
| 95 |
+
<Sigma size={18} />
|
| 96 |
</Btn>
|
| 97 |
|
| 98 |
+
<span className="divider-v" />
|
| 99 |
|
| 100 |
<Btn
|
| 101 |
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
| 102 |
active={editor.isActive("heading", { level: 2 })}
|
| 103 |
tooltip="Heading"
|
| 104 |
>
|
| 105 |
+
<Heading2 size={18} />
|
| 106 |
</Btn>
|
| 107 |
<Btn
|
| 108 |
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
| 109 |
active={editor.isActive("blockquote")}
|
| 110 |
tooltip="Quote"
|
| 111 |
>
|
| 112 |
+
<Quote size={18} />
|
| 113 |
</Btn>
|
| 114 |
<Btn
|
| 115 |
onClick={setLink}
|
| 116 |
active={editor.isActive("link")}
|
| 117 |
tooltip="Link"
|
| 118 |
>
|
| 119 |
+
<Link size={18} />
|
| 120 |
</Btn>
|
| 121 |
|
| 122 |
{onAddComment && (
|
| 123 |
<>
|
| 124 |
+
<span className="divider-v" />
|
| 125 |
+
<Btn onClick={onAddComment} tooltip="Comment">
|
| 126 |
+
<MessageCircle size={18} />
|
|
|
|
|
|
|
|
|
|
| 127 |
</Btn>
|
| 128 |
</>
|
| 129 |
)}
|
| 130 |
+
</div>
|
| 131 |
</BubbleMenu>
|
| 132 |
);
|
| 133 |
}
|
|
@@ -1,15 +1,18 @@
|
|
| 1 |
import { FloatingMenu } from "@tiptap/react";
|
| 2 |
import type { Editor } from "@tiptap/core";
|
| 3 |
import { useState } from "react";
|
| 4 |
-
import {
|
| 5 |
-
import
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
interface FloatingActionsProps {
|
| 15 |
editor: Editor;
|
|
@@ -32,112 +35,94 @@ export function FloatingActions({ editor }: FloatingActionsProps) {
|
|
| 32 |
offset: [-4, 0],
|
| 33 |
}}
|
| 34 |
>
|
| 35 |
-
<
|
| 36 |
-
<Tooltip title="Insert block"
|
| 37 |
-
<
|
| 38 |
-
|
| 39 |
onClick={() => setOpen(!open)}
|
| 40 |
-
|
| 41 |
-
color: open ? "text.primary" : "text.disabled",
|
| 42 |
-
border: "1px solid",
|
| 43 |
-
borderColor: open ? "divider" : "transparent",
|
| 44 |
-
transition: "all 0.15s",
|
| 45 |
-
"&:hover": { color: "text.primary", borderColor: "divider" },
|
| 46 |
-
transform: open ? "rotate(45deg)" : "none",
|
| 47 |
-
p: 0.5,
|
| 48 |
-
}}
|
| 49 |
>
|
| 50 |
-
<
|
| 51 |
-
</
|
| 52 |
</Tooltip>
|
| 53 |
|
| 54 |
-
|
| 55 |
-
<
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
gap: 0.25,
|
| 60 |
-
px: 0.5,
|
| 61 |
-
py: 0.25,
|
| 62 |
-
bgcolor: "background.paper",
|
| 63 |
-
boxShadow: "0 4px 20px rgba(0,0,0,0.3)",
|
| 64 |
-
}}
|
| 65 |
-
>
|
| 66 |
-
<Tooltip title="Heading 2" arrow>
|
| 67 |
-
<IconButton
|
| 68 |
-
size="small"
|
| 69 |
onClick={() => insert(() => editor.chain().focus().toggleHeading({ level: 2 }).run())}
|
| 70 |
-
|
| 71 |
>
|
| 72 |
-
<
|
| 73 |
-
</
|
| 74 |
</Tooltip>
|
| 75 |
-
<Tooltip title="Heading 3"
|
| 76 |
-
<
|
| 77 |
-
|
| 78 |
onClick={() => insert(() => editor.chain().focus().toggleHeading({ level: 3 }).run())}
|
| 79 |
-
|
| 80 |
>
|
| 81 |
-
|
| 82 |
-
</
|
| 83 |
</Tooltip>
|
| 84 |
-
<Tooltip title="Quote"
|
| 85 |
-
<
|
| 86 |
-
|
| 87 |
onClick={() => insert(() => editor.chain().focus().toggleBlockquote().run())}
|
| 88 |
-
|
| 89 |
>
|
| 90 |
-
<
|
| 91 |
-
</
|
| 92 |
</Tooltip>
|
| 93 |
-
<Tooltip title="Bullet list"
|
| 94 |
-
<
|
| 95 |
-
|
| 96 |
onClick={() => insert(() => editor.chain().focus().toggleBulletList().run())}
|
| 97 |
-
|
| 98 |
>
|
| 99 |
-
<
|
| 100 |
-
</
|
| 101 |
</Tooltip>
|
| 102 |
-
<Tooltip title="Numbered list"
|
| 103 |
-
<
|
| 104 |
-
|
| 105 |
onClick={() => insert(() => editor.chain().focus().toggleOrderedList().run())}
|
| 106 |
-
|
| 107 |
>
|
| 108 |
-
<
|
| 109 |
-
</
|
| 110 |
</Tooltip>
|
| 111 |
-
<Tooltip title="Code block"
|
| 112 |
-
<
|
| 113 |
-
|
| 114 |
onClick={() => insert(() => editor.chain().focus().toggleCodeBlock().run())}
|
| 115 |
-
|
| 116 |
>
|
| 117 |
-
<
|
| 118 |
-
</
|
| 119 |
</Tooltip>
|
| 120 |
-
<Tooltip title="Equation"
|
| 121 |
-
<
|
| 122 |
-
|
| 123 |
onClick={() => insert(() => editor.chain().focus().insertBlockMath({ latex: "E = mc^2" }).run())}
|
| 124 |
-
|
| 125 |
>
|
| 126 |
-
<
|
| 127 |
-
</
|
| 128 |
</Tooltip>
|
| 129 |
-
<Tooltip title="Divider"
|
| 130 |
-
<
|
| 131 |
-
|
| 132 |
onClick={() => insert(() => editor.chain().focus().setHorizontalRule().run())}
|
| 133 |
-
|
| 134 |
>
|
| 135 |
-
<
|
| 136 |
-
</
|
| 137 |
</Tooltip>
|
| 138 |
-
</
|
| 139 |
-
|
| 140 |
-
</
|
| 141 |
</FloatingMenu>
|
| 142 |
);
|
| 143 |
}
|
|
|
|
| 1 |
import { FloatingMenu } from "@tiptap/react";
|
| 2 |
import type { Editor } from "@tiptap/core";
|
| 3 |
import { useState } from "react";
|
| 4 |
+
import { Tooltip } from "../components/Tooltip";
|
| 5 |
+
import {
|
| 6 |
+
Plus,
|
| 7 |
+
Heading2,
|
| 8 |
+
Heading3,
|
| 9 |
+
Quote,
|
| 10 |
+
List,
|
| 11 |
+
ListOrdered,
|
| 12 |
+
Braces,
|
| 13 |
+
Minus,
|
| 14 |
+
Sigma,
|
| 15 |
+
} from "lucide-react";
|
| 16 |
|
| 17 |
interface FloatingActionsProps {
|
| 18 |
editor: Editor;
|
|
|
|
| 35 |
offset: [-4, 0],
|
| 36 |
}}
|
| 37 |
>
|
| 38 |
+
<div className="floating-actions">
|
| 39 |
+
<Tooltip title="Insert block" placement="left">
|
| 40 |
+
<button
|
| 41 |
+
className={`floating-actions__toggle ${open ? "floating-actions__toggle--open" : ""}`}
|
| 42 |
onClick={() => setOpen(!open)}
|
| 43 |
+
aria-label="Insert block"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
>
|
| 45 |
+
<Plus />
|
| 46 |
+
</button>
|
| 47 |
</Tooltip>
|
| 48 |
|
| 49 |
+
{open && (
|
| 50 |
+
<div className="floating-actions__panel surface">
|
| 51 |
+
<Tooltip title="Heading 2">
|
| 52 |
+
<button
|
| 53 |
+
className="icon-btn"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
onClick={() => insert(() => editor.chain().focus().toggleHeading({ level: 2 }).run())}
|
| 55 |
+
aria-label="Heading 2"
|
| 56 |
>
|
| 57 |
+
<Heading2 size={18} />
|
| 58 |
+
</button>
|
| 59 |
</Tooltip>
|
| 60 |
+
<Tooltip title="Heading 3">
|
| 61 |
+
<button
|
| 62 |
+
className="icon-btn"
|
| 63 |
onClick={() => insert(() => editor.chain().focus().toggleHeading({ level: 3 }).run())}
|
| 64 |
+
aria-label="Heading 3"
|
| 65 |
>
|
| 66 |
+
<Heading3 size={18} />
|
| 67 |
+
</button>
|
| 68 |
</Tooltip>
|
| 69 |
+
<Tooltip title="Quote">
|
| 70 |
+
<button
|
| 71 |
+
className="icon-btn"
|
| 72 |
onClick={() => insert(() => editor.chain().focus().toggleBlockquote().run())}
|
| 73 |
+
aria-label="Quote"
|
| 74 |
>
|
| 75 |
+
<Quote size={18} />
|
| 76 |
+
</button>
|
| 77 |
</Tooltip>
|
| 78 |
+
<Tooltip title="Bullet list">
|
| 79 |
+
<button
|
| 80 |
+
className="icon-btn"
|
| 81 |
onClick={() => insert(() => editor.chain().focus().toggleBulletList().run())}
|
| 82 |
+
aria-label="Bullet list"
|
| 83 |
>
|
| 84 |
+
<List size={18} />
|
| 85 |
+
</button>
|
| 86 |
</Tooltip>
|
| 87 |
+
<Tooltip title="Numbered list">
|
| 88 |
+
<button
|
| 89 |
+
className="icon-btn"
|
| 90 |
onClick={() => insert(() => editor.chain().focus().toggleOrderedList().run())}
|
| 91 |
+
aria-label="Numbered list"
|
| 92 |
>
|
| 93 |
+
<ListOrdered size={18} />
|
| 94 |
+
</button>
|
| 95 |
</Tooltip>
|
| 96 |
+
<Tooltip title="Code block">
|
| 97 |
+
<button
|
| 98 |
+
className="icon-btn"
|
| 99 |
onClick={() => insert(() => editor.chain().focus().toggleCodeBlock().run())}
|
| 100 |
+
aria-label="Code block"
|
| 101 |
>
|
| 102 |
+
<Braces size={18} />
|
| 103 |
+
</button>
|
| 104 |
</Tooltip>
|
| 105 |
+
<Tooltip title="Equation">
|
| 106 |
+
<button
|
| 107 |
+
className="icon-btn"
|
| 108 |
onClick={() => insert(() => editor.chain().focus().insertBlockMath({ latex: "E = mc^2" }).run())}
|
| 109 |
+
aria-label="Equation"
|
| 110 |
>
|
| 111 |
+
<Sigma size={18} />
|
| 112 |
+
</button>
|
| 113 |
</Tooltip>
|
| 114 |
+
<Tooltip title="Divider">
|
| 115 |
+
<button
|
| 116 |
+
className="icon-btn"
|
| 117 |
onClick={() => insert(() => editor.chain().focus().setHorizontalRule().run())}
|
| 118 |
+
aria-label="Divider"
|
| 119 |
>
|
| 120 |
+
<Minus size={18} />
|
| 121 |
+
</button>
|
| 122 |
</Tooltip>
|
| 123 |
+
</div>
|
| 124 |
+
)}
|
| 125 |
+
</div>
|
| 126 |
</FloatingMenu>
|
| 127 |
);
|
| 128 |
}
|
|
@@ -4,8 +4,13 @@
|
|
| 4 |
// Each entry describes a custom MDX component that can be used inside the
|
| 5 |
// editor. Factories (factory.ts) consume these definitions to auto-generate
|
| 6 |
// TipTap extensions, NodeViews, slash-menu items and MDX serializers.
|
|
|
|
|
|
|
|
|
|
| 7 |
// ---------------------------------------------------------------------------
|
| 8 |
|
|
|
|
|
|
|
| 9 |
export interface ComponentField {
|
| 10 |
name: string;
|
| 11 |
type: "string" | "boolean" | "select";
|
|
@@ -34,141 +39,87 @@ export interface ComponentDef {
|
|
| 34 |
content?: string;
|
| 35 |
}
|
| 36 |
|
| 37 |
-
//
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
-
{
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
icon: "▸",
|
| 46 |
-
label: "Accordion",
|
| 47 |
-
description: "Collapsible content section",
|
| 48 |
-
kind: "wrapper",
|
| 49 |
-
fields: [
|
| 50 |
-
{ name: "title", type: "string", label: "Title", default: "Details", placeholder: "Section title…" },
|
| 51 |
-
{ name: "open", type: "boolean", label: "Open by default", default: false },
|
| 52 |
-
],
|
| 53 |
},
|
| 54 |
-
{
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
icon: "ℹ",
|
| 58 |
-
label: "Note / Callout",
|
| 59 |
-
description: "Highlighted callout box",
|
| 60 |
-
kind: "wrapper",
|
| 61 |
-
fields: [
|
| 62 |
-
{ name: "title", type: "string", label: "Title", default: "", placeholder: "Optional title…" },
|
| 63 |
-
{ name: "emoji", type: "string", label: "Emoji", default: "", placeholder: "e.g. 💡" },
|
| 64 |
-
{
|
| 65 |
-
name: "variant",
|
| 66 |
-
type: "select",
|
| 67 |
-
label: "Variant",
|
| 68 |
-
default: "neutral",
|
| 69 |
-
options: ["neutral", "info", "success", "danger"],
|
| 70 |
-
},
|
| 71 |
-
],
|
| 72 |
},
|
| 73 |
-
{
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
icon: "❝",
|
| 77 |
-
label: "Quote block",
|
| 78 |
-
description: "Quote with author attribution",
|
| 79 |
-
kind: "wrapper",
|
| 80 |
-
fields: [
|
| 81 |
-
{ name: "author", type: "string", label: "Author", default: "", placeholder: "Author name…" },
|
| 82 |
-
{ name: "source", type: "string", label: "Source", default: "", placeholder: "Book, URL…" },
|
| 83 |
-
],
|
| 84 |
},
|
| 85 |
-
{
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
icon: "↔",
|
| 89 |
-
label: "Wide",
|
| 90 |
-
description: "Wider-than-column container",
|
| 91 |
-
kind: "wrapper",
|
| 92 |
-
fields: [],
|
| 93 |
},
|
| 94 |
-
{
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
icon: "⟷",
|
| 98 |
-
label: "Full width",
|
| 99 |
-
description: "Full-width container",
|
| 100 |
-
kind: "wrapper",
|
| 101 |
-
fields: [],
|
| 102 |
},
|
| 103 |
-
{
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
icon: "¶",
|
| 107 |
-
label: "Sidenote",
|
| 108 |
-
description: "Margin note alongside content",
|
| 109 |
-
kind: "wrapper",
|
| 110 |
-
fields: [],
|
| 111 |
},
|
| 112 |
-
{
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
icon: "🏷",
|
| 116 |
-
label: "Reference / Figure",
|
| 117 |
-
description: "Captioned figure with anchor ID",
|
| 118 |
-
kind: "wrapper",
|
| 119 |
-
fields: [
|
| 120 |
-
{ name: "id", type: "string", label: "ID", default: "", placeholder: "fig-1" },
|
| 121 |
-
{ name: "caption", type: "string", label: "Caption", default: "", placeholder: "Figure caption…" },
|
| 122 |
-
],
|
| 123 |
},
|
| 124 |
-
{
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
icon: "📊",
|
| 128 |
-
label: "HTML Embed",
|
| 129 |
-
description: "Embed an external HTML visualization",
|
| 130 |
-
kind: "atomic",
|
| 131 |
-
fields: [
|
| 132 |
-
{ name: "src", type: "string", label: "Source file", default: "", placeholder: "d3-chart.html" },
|
| 133 |
-
{ name: "title", type: "string", label: "Title", default: "", placeholder: "Chart title…" },
|
| 134 |
-
{ name: "desc", type: "string", label: "Description", default: "", placeholder: "Chart description…" },
|
| 135 |
-
{ name: "wide", type: "boolean", label: "Wide", default: false },
|
| 136 |
-
{ name: "downloadable", type: "boolean", label: "Downloadable", default: false },
|
| 137 |
-
],
|
| 138 |
},
|
| 139 |
-
{
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
icon: "👤",
|
| 143 |
-
label: "HF User card",
|
| 144 |
-
description: "Hugging Face user profile card",
|
| 145 |
-
kind: "atomic",
|
| 146 |
-
fields: [
|
| 147 |
-
{ name: "username", type: "string", label: "Username", default: "", placeholder: "username" },
|
| 148 |
-
{ name: "name", type: "string", label: "Display name", default: "", placeholder: "Full Name" },
|
| 149 |
-
{ name: "url", type: "string", label: "URL", default: "", placeholder: "https://huggingface.co/username" },
|
| 150 |
-
],
|
| 151 |
},
|
| 152 |
-
{
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
icon: "</>",
|
| 156 |
-
label: "Raw HTML",
|
| 157 |
-
description: "Inject raw HTML content",
|
| 158 |
-
kind: "atomic",
|
| 159 |
-
fields: [
|
| 160 |
-
{ name: "html", type: "string", label: "HTML", default: "", placeholder: "<div>…</div>" },
|
| 161 |
-
],
|
| 162 |
},
|
| 163 |
-
{
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
icon: "◈",
|
| 167 |
-
label: "Mermaid diagram",
|
| 168 |
-
description: "Flowchart, sequence, Gantt, etc.",
|
| 169 |
-
kind: "atomic",
|
| 170 |
-
fields: [
|
| 171 |
-
{ name: "code", type: "string", label: "Code", default: "graph TD\n A[Start] --> B{Decision}\n B -->|Yes| C[OK]\n B -->|No| D[End]", placeholder: "graph TD\\n A --> B" },
|
| 172 |
-
],
|
| 173 |
},
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
// Each entry describes a custom MDX component that can be used inside the
|
| 5 |
// editor. Factories (factory.ts) consume these definitions to auto-generate
|
| 6 |
// TipTap extensions, NodeViews, slash-menu items and MDX serializers.
|
| 7 |
+
//
|
| 8 |
+
// Structural data (name, kind, fields, content) comes from the shared module.
|
| 9 |
+
// This file adds UI metadata (tag, icon, label, description, placeholders).
|
| 10 |
// ---------------------------------------------------------------------------
|
| 11 |
|
| 12 |
+
import { SHARED_COMPONENT_DEFS, type SharedComponentDef } from "#shared/component-defs";
|
| 13 |
+
|
| 14 |
export interface ComponentField {
|
| 15 |
name: string;
|
| 16 |
type: "string" | "boolean" | "select";
|
|
|
|
| 39 |
content?: string;
|
| 40 |
}
|
| 41 |
|
| 42 |
+
// UI metadata per component (tag, icon, label, description, field labels/placeholders)
|
| 43 |
+
interface UIMeta {
|
| 44 |
+
tag: string;
|
| 45 |
+
icon: string;
|
| 46 |
+
label: string;
|
| 47 |
+
description: string;
|
| 48 |
+
fieldMeta: Record<string, { label: string; placeholder?: string }>;
|
| 49 |
+
}
|
| 50 |
|
| 51 |
+
const UI_META: Record<string, UIMeta> = {
|
| 52 |
+
accordion: {
|
| 53 |
+
tag: "Accordion", icon: "▸", label: "Accordion", description: "Collapsible content section",
|
| 54 |
+
fieldMeta: { title: { label: "Title", placeholder: "Section title…" }, open: { label: "Open by default" } },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
},
|
| 56 |
+
note: {
|
| 57 |
+
tag: "Note", icon: "ℹ", label: "Note / Callout", description: "Highlighted callout box",
|
| 58 |
+
fieldMeta: { title: { label: "Title", placeholder: "Optional title…" }, emoji: { label: "Emoji", placeholder: "e.g. 💡" }, variant: { label: "Variant" } },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
},
|
| 60 |
+
quoteBlock: {
|
| 61 |
+
tag: "Quote", icon: "❝", label: "Quote block", description: "Quote with author attribution",
|
| 62 |
+
fieldMeta: { author: { label: "Author", placeholder: "Author name…" }, source: { label: "Source", placeholder: "Book, URL…" } },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
},
|
| 64 |
+
wide: {
|
| 65 |
+
tag: "Wide", icon: "↔", label: "Wide", description: "Wider-than-column container",
|
| 66 |
+
fieldMeta: {},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
},
|
| 68 |
+
fullWidth: {
|
| 69 |
+
tag: "FullWidth", icon: "⟷", label: "Full width", description: "Full-width container",
|
| 70 |
+
fieldMeta: {},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
},
|
| 72 |
+
sidenote: {
|
| 73 |
+
tag: "Sidenote", icon: "¶", label: "Sidenote", description: "Margin note alongside content",
|
| 74 |
+
fieldMeta: {},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
},
|
| 76 |
+
reference: {
|
| 77 |
+
tag: "Reference", icon: "🏷", label: "Reference / Figure", description: "Captioned figure with anchor ID",
|
| 78 |
+
fieldMeta: { id: { label: "ID", placeholder: "fig-1" }, caption: { label: "Caption", placeholder: "Figure caption…" } },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
},
|
| 80 |
+
htmlEmbed: {
|
| 81 |
+
tag: "HtmlEmbed", icon: "📊", label: "HTML Embed", description: "Embed an external HTML visualization",
|
| 82 |
+
fieldMeta: { src: { label: "Source file", placeholder: "d3-chart.html" }, title: { label: "Title", placeholder: "Chart title…" }, desc: { label: "Description", placeholder: "Chart description…" }, wide: { label: "Wide" }, downloadable: { label: "Downloadable" } },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
},
|
| 84 |
+
hfUser: {
|
| 85 |
+
tag: "HfUser", icon: "👤", label: "HF User card", description: "Hugging Face user profile card",
|
| 86 |
+
fieldMeta: { username: { label: "Username", placeholder: "username" }, name: { label: "Display name", placeholder: "Full Name" }, url: { label: "URL", placeholder: "https://huggingface.co/username" } },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
},
|
| 88 |
+
rawHtml: {
|
| 89 |
+
tag: "RawHtml", icon: "</>", label: "Raw HTML", description: "Inject raw HTML content",
|
| 90 |
+
fieldMeta: { html: { label: "HTML", placeholder: "<div>…</div>" } },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
},
|
| 92 |
+
mermaid: {
|
| 93 |
+
tag: "Mermaid", icon: "◈", label: "Mermaid diagram", description: "Flowchart, sequence, Gantt, etc.",
|
| 94 |
+
fieldMeta: { code: { label: "Code", placeholder: "graph TD\\n A --> B" } },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
},
|
| 96 |
+
};
|
| 97 |
+
|
| 98 |
+
function buildComponentDef(shared: SharedComponentDef): ComponentDef {
|
| 99 |
+
const ui = UI_META[shared.name];
|
| 100 |
+
if (!ui) throw new Error(`Missing UI_META for component "${shared.name}"`);
|
| 101 |
+
|
| 102 |
+
return {
|
| 103 |
+
name: shared.name,
|
| 104 |
+
tag: ui.tag,
|
| 105 |
+
icon: ui.icon,
|
| 106 |
+
label: ui.label,
|
| 107 |
+
description: ui.description,
|
| 108 |
+
kind: shared.kind,
|
| 109 |
+
content: shared.content,
|
| 110 |
+
fields: shared.fields.map((f) => ({
|
| 111 |
+
name: f.name,
|
| 112 |
+
type: f.type,
|
| 113 |
+
default: f.default,
|
| 114 |
+
options: f.options,
|
| 115 |
+
label: ui.fieldMeta[f.name]?.label ?? f.name,
|
| 116 |
+
placeholder: ui.fieldMeta[f.name]?.placeholder,
|
| 117 |
+
})),
|
| 118 |
+
};
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// ---------------------------------------------------------------------------
|
| 122 |
+
// Registry - built from shared definitions + UI metadata
|
| 123 |
+
// ---------------------------------------------------------------------------
|
| 124 |
+
|
| 125 |
+
export const COMPONENTS: ComponentDef[] = SHARED_COMPONENT_DEFS.map(buildComponentDef);
|
|
@@ -1,14 +1,6 @@
|
|
| 1 |
import { useState, useRef, useEffect, type KeyboardEvent } from "react";
|
| 2 |
-
import {
|
| 3 |
-
|
| 4 |
-
Chip,
|
| 5 |
-
IconButton,
|
| 6 |
-
TextField,
|
| 7 |
-
Tooltip,
|
| 8 |
-
Typography,
|
| 9 |
-
} from "@mui/material";
|
| 10 |
-
import AddIcon from "@mui/icons-material/Add";
|
| 11 |
-
import CloseIcon from "@mui/icons-material/Close";
|
| 12 |
import { useFrontmatter } from "./useFrontmatter";
|
| 13 |
import type { FrontmatterStore, Author, Affiliation } from "./frontmatter-store";
|
| 14 |
|
|
@@ -32,7 +24,6 @@ export function FrontmatterHero({ store }: FrontmatterHeroProps) {
|
|
| 32 |
|
| 33 |
return (
|
| 34 |
<div>
|
| 35 |
-
{/* Hero section - uses .hero from _hero.css (same as HeroArticle.astro) */}
|
| 36 |
<section className="hero">
|
| 37 |
<EditableText
|
| 38 |
value={data.title}
|
|
@@ -50,11 +41,9 @@ export function FrontmatterHero({ store }: FrontmatterHeroProps) {
|
|
| 50 |
/>
|
| 51 |
</section>
|
| 52 |
|
| 53 |
-
{/* Meta bar - uses .meta from _hero.css (same as HeroArticle.astro) */}
|
| 54 |
{hasMeta && (
|
| 55 |
<header className="meta" aria-label="Article meta information">
|
| 56 |
<div className="meta-container">
|
| 57 |
-
{/* Authors cell */}
|
| 58 |
<div className="meta-container-cell">
|
| 59 |
<h3>Authors</h3>
|
| 60 |
<ul className="authors">
|
|
@@ -79,20 +68,20 @@ export function FrontmatterHero({ store }: FrontmatterHeroProps) {
|
|
| 79 |
</li>
|
| 80 |
))}
|
| 81 |
<li className="author-add-btn">
|
| 82 |
-
<Tooltip title="Add author"
|
| 83 |
-
<
|
| 84 |
-
|
| 85 |
onClick={() => setShowAuthorForm(true)}
|
| 86 |
-
|
|
|
|
| 87 |
>
|
| 88 |
-
<
|
| 89 |
-
</
|
| 90 |
</Tooltip>
|
| 91 |
</li>
|
| 92 |
</ul>
|
| 93 |
</div>
|
| 94 |
|
| 95 |
-
{/* Affiliations cell */}
|
| 96 |
{hasAffiliations && (
|
| 97 |
<div className="meta-container-cell">
|
| 98 |
<h3>Affiliations</h3>
|
|
@@ -124,7 +113,6 @@ export function FrontmatterHero({ store }: FrontmatterHeroProps) {
|
|
| 124 |
</div>
|
| 125 |
)}
|
| 126 |
|
| 127 |
-
{/* Published cell */}
|
| 128 |
<div className="meta-container-cell">
|
| 129 |
<h3>Published</h3>
|
| 130 |
<EditableText
|
|
@@ -135,7 +123,6 @@ export function FrontmatterHero({ store }: FrontmatterHeroProps) {
|
|
| 135 |
/>
|
| 136 |
</div>
|
| 137 |
|
| 138 |
-
{/* DOI cell */}
|
| 139 |
<div className="meta-container-cell">
|
| 140 |
<h3>DOI</h3>
|
| 141 |
<EditableText
|
|
@@ -145,12 +132,10 @@ export function FrontmatterHero({ store }: FrontmatterHeroProps) {
|
|
| 145 |
inline
|
| 146 |
/>
|
| 147 |
</div>
|
| 148 |
-
|
| 149 |
</div>
|
| 150 |
</header>
|
| 151 |
)}
|
| 152 |
|
| 153 |
-
{/* Author forms (editor-only, MUI is fine here) */}
|
| 154 |
<div style={{ maxWidth: 680, margin: "0 auto", padding: "0 16px" }}>
|
| 155 |
{showAuthorForm && (
|
| 156 |
<AuthorInlineForm
|
|
@@ -187,8 +172,6 @@ export function FrontmatterHero({ store }: FrontmatterHeroProps) {
|
|
| 187 |
);
|
| 188 |
}
|
| 189 |
|
| 190 |
-
// ---- Editable text field (click-to-edit, editor-only behavior) ----
|
| 191 |
-
|
| 192 |
interface EditableTextProps {
|
| 193 |
value: string;
|
| 194 |
placeholder: string;
|
|
@@ -237,23 +220,28 @@ function EditableText({ value, placeholder, onChange, className, multiline, inli
|
|
| 237 |
const isEmpty = !value;
|
| 238 |
|
| 239 |
if (editing) {
|
|
|
|
| 240 |
return (
|
| 241 |
<span className={className} style={{ display: inline ? "inline-flex" : "flex" }}>
|
| 242 |
-
<
|
| 243 |
-
|
|
|
|
| 244 |
value={draft}
|
| 245 |
onChange={(e) => setDraft(e.target.value)}
|
| 246 |
onBlur={commit}
|
| 247 |
onKeyDown={handleKeyDown}
|
| 248 |
-
multiline={multiline}
|
| 249 |
-
fullWidth={!inline}
|
| 250 |
-
variant="standard"
|
| 251 |
placeholder={placeholder}
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
}}
|
| 258 |
/>
|
| 259 |
</span>
|
|
@@ -275,8 +263,6 @@ function EditableText({ value, placeholder, onChange, className, multiline, inli
|
|
| 275 |
);
|
| 276 |
}
|
| 277 |
|
| 278 |
-
// ---- Author inline form (editor-only, MUI is fine) ----
|
| 279 |
-
|
| 280 |
function AuthorInlineForm({
|
| 281 |
initial,
|
| 282 |
affiliations,
|
|
@@ -314,78 +300,62 @@ function AuthorInlineForm({
|
|
| 314 |
};
|
| 315 |
|
| 316 |
return (
|
| 317 |
-
<
|
| 318 |
-
|
| 319 |
-
mt: 2,
|
| 320 |
-
p: 2,
|
| 321 |
-
border: "1px solid",
|
| 322 |
-
borderColor: "divider",
|
| 323 |
-
borderRadius: 2,
|
| 324 |
-
bgcolor: "background.paper",
|
| 325 |
-
display: "flex",
|
| 326 |
-
flexDirection: "column",
|
| 327 |
-
gap: 1.5,
|
| 328 |
-
}}
|
| 329 |
-
>
|
| 330 |
-
<Typography variant="caption" sx={{ fontWeight: 600, color: "text.secondary", textTransform: "uppercase", letterSpacing: "0.05em" }}>
|
| 331 |
{initial ? "Edit author" : "Add author"}
|
| 332 |
-
</
|
| 333 |
|
| 334 |
-
<
|
| 335 |
-
<
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
value={name}
|
| 340 |
onChange={(e) => setName(e.target.value)}
|
| 341 |
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
|
| 342 |
-
fullWidth
|
| 343 |
/>
|
| 344 |
-
<
|
| 345 |
-
|
| 346 |
-
|
| 347 |
value={url}
|
| 348 |
onChange={(e) => setUrl(e.target.value)}
|
| 349 |
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
|
| 350 |
-
fullWidth
|
| 351 |
/>
|
| 352 |
-
</
|
| 353 |
|
| 354 |
{affiliations.length > 0 && (
|
| 355 |
-
<
|
| 356 |
{affiliations.map((aff, i) => (
|
| 357 |
-
<
|
| 358 |
key={`aff-select-${i}`}
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
variant={affIndices.includes(i + 1) ? "filled" : "outlined"}
|
| 362 |
onClick={() => toggleAff(i + 1)}
|
| 363 |
-
|
| 364 |
-
|
|
|
|
| 365 |
))}
|
| 366 |
-
</
|
| 367 |
)}
|
| 368 |
|
| 369 |
-
<
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
placeholder="e.g. Hugging Face"
|
| 373 |
value={newAffName}
|
| 374 |
onChange={(e) => setNewAffName(e.target.value)}
|
| 375 |
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
|
| 376 |
/>
|
| 377 |
|
| 378 |
-
<
|
| 379 |
-
<
|
| 380 |
-
<
|
| 381 |
-
|
| 382 |
-
size="small"
|
| 383 |
-
color="primary"
|
| 384 |
onClick={handleSubmit}
|
| 385 |
disabled={!name.trim()}
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
|
|
|
| 390 |
);
|
| 391 |
}
|
|
|
|
| 1 |
import { useState, useRef, useEffect, type KeyboardEvent } from "react";
|
| 2 |
+
import { Tooltip } from "../../components/Tooltip";
|
| 3 |
+
import { Plus, X } from "lucide-react";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import { useFrontmatter } from "./useFrontmatter";
|
| 5 |
import type { FrontmatterStore, Author, Affiliation } from "./frontmatter-store";
|
| 6 |
|
|
|
|
| 24 |
|
| 25 |
return (
|
| 26 |
<div>
|
|
|
|
| 27 |
<section className="hero">
|
| 28 |
<EditableText
|
| 29 |
value={data.title}
|
|
|
|
| 41 |
/>
|
| 42 |
</section>
|
| 43 |
|
|
|
|
| 44 |
{hasMeta && (
|
| 45 |
<header className="meta" aria-label="Article meta information">
|
| 46 |
<div className="meta-container">
|
|
|
|
| 47 |
<div className="meta-container-cell">
|
| 48 |
<h3>Authors</h3>
|
| 49 |
<ul className="authors">
|
|
|
|
| 68 |
</li>
|
| 69 |
))}
|
| 70 |
<li className="author-add-btn">
|
| 71 |
+
<Tooltip title="Add author">
|
| 72 |
+
<button
|
| 73 |
+
className="icon-btn"
|
| 74 |
onClick={() => setShowAuthorForm(true)}
|
| 75 |
+
aria-label="Add author"
|
| 76 |
+
style={{ padding: 2 }}
|
| 77 |
>
|
| 78 |
+
<Plus size={14} />
|
| 79 |
+
</button>
|
| 80 |
</Tooltip>
|
| 81 |
</li>
|
| 82 |
</ul>
|
| 83 |
</div>
|
| 84 |
|
|
|
|
| 85 |
{hasAffiliations && (
|
| 86 |
<div className="meta-container-cell">
|
| 87 |
<h3>Affiliations</h3>
|
|
|
|
| 113 |
</div>
|
| 114 |
)}
|
| 115 |
|
|
|
|
| 116 |
<div className="meta-container-cell">
|
| 117 |
<h3>Published</h3>
|
| 118 |
<EditableText
|
|
|
|
| 123 |
/>
|
| 124 |
</div>
|
| 125 |
|
|
|
|
| 126 |
<div className="meta-container-cell">
|
| 127 |
<h3>DOI</h3>
|
| 128 |
<EditableText
|
|
|
|
| 132 |
inline
|
| 133 |
/>
|
| 134 |
</div>
|
|
|
|
| 135 |
</div>
|
| 136 |
</header>
|
| 137 |
)}
|
| 138 |
|
|
|
|
| 139 |
<div style={{ maxWidth: 680, margin: "0 auto", padding: "0 16px" }}>
|
| 140 |
{showAuthorForm && (
|
| 141 |
<AuthorInlineForm
|
|
|
|
| 172 |
);
|
| 173 |
}
|
| 174 |
|
|
|
|
|
|
|
| 175 |
interface EditableTextProps {
|
| 176 |
value: string;
|
| 177 |
placeholder: string;
|
|
|
|
| 220 |
const isEmpty = !value;
|
| 221 |
|
| 222 |
if (editing) {
|
| 223 |
+
const Tag = multiline ? "textarea" : "input";
|
| 224 |
return (
|
| 225 |
<span className={className} style={{ display: inline ? "inline-flex" : "flex" }}>
|
| 226 |
+
<Tag
|
| 227 |
+
ref={inputRef as any}
|
| 228 |
+
className="form-input"
|
| 229 |
value={draft}
|
| 230 |
onChange={(e) => setDraft(e.target.value)}
|
| 231 |
onBlur={commit}
|
| 232 |
onKeyDown={handleKeyDown}
|
|
|
|
|
|
|
|
|
|
| 233 |
placeholder={placeholder}
|
| 234 |
+
style={{
|
| 235 |
+
font: "inherit",
|
| 236 |
+
color: "inherit",
|
| 237 |
+
textAlign: "inherit",
|
| 238 |
+
background: "transparent",
|
| 239 |
+
border: "none",
|
| 240 |
+
padding: 0,
|
| 241 |
+
margin: 0,
|
| 242 |
+
width: "100%",
|
| 243 |
+
outline: "none",
|
| 244 |
+
resize: multiline ? "vertical" : "none",
|
| 245 |
}}
|
| 246 |
/>
|
| 247 |
</span>
|
|
|
|
| 263 |
);
|
| 264 |
}
|
| 265 |
|
|
|
|
|
|
|
| 266 |
function AuthorInlineForm({
|
| 267 |
initial,
|
| 268 |
affiliations,
|
|
|
|
| 300 |
};
|
| 301 |
|
| 302 |
return (
|
| 303 |
+
<div className="author-form">
|
| 304 |
+
<span className="author-form__title">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
{initial ? "Edit author" : "Add author"}
|
| 306 |
+
</span>
|
| 307 |
|
| 308 |
+
<div className="author-form__row">
|
| 309 |
+
<input
|
| 310 |
+
ref={nameRef}
|
| 311 |
+
className="form-input"
|
| 312 |
+
placeholder="Name"
|
| 313 |
value={name}
|
| 314 |
onChange={(e) => setName(e.target.value)}
|
| 315 |
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
|
|
|
|
| 316 |
/>
|
| 317 |
+
<input
|
| 318 |
+
className="form-input"
|
| 319 |
+
placeholder="URL (optional)"
|
| 320 |
value={url}
|
| 321 |
onChange={(e) => setUrl(e.target.value)}
|
| 322 |
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
|
|
|
|
| 323 |
/>
|
| 324 |
+
</div>
|
| 325 |
|
| 326 |
{affiliations.length > 0 && (
|
| 327 |
+
<div className="author-form__chips">
|
| 328 |
{affiliations.map((aff, i) => (
|
| 329 |
+
<span
|
| 330 |
key={`aff-select-${i}`}
|
| 331 |
+
className={`chip chip--clickable ${affIndices.includes(i + 1) ? "" : "chip--outlined"}`}
|
| 332 |
+
style={affIndices.includes(i + 1) ? { background: "var(--primary-color)", color: "#000" } : {}}
|
|
|
|
| 333 |
onClick={() => toggleAff(i + 1)}
|
| 334 |
+
>
|
| 335 |
+
{i + 1}. {aff.name}
|
| 336 |
+
</span>
|
| 337 |
))}
|
| 338 |
+
</div>
|
| 339 |
)}
|
| 340 |
|
| 341 |
+
<input
|
| 342 |
+
className="form-input"
|
| 343 |
+
placeholder="New affiliation (optional), e.g. Hugging Face"
|
|
|
|
| 344 |
value={newAffName}
|
| 345 |
onChange={(e) => setNewAffName(e.target.value)}
|
| 346 |
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
|
| 347 |
/>
|
| 348 |
|
| 349 |
+
<div className="author-form__actions">
|
| 350 |
+
<button className="btn" onClick={onCancel}>Cancel</button>
|
| 351 |
+
<button
|
| 352 |
+
className="btn btn--primary"
|
|
|
|
|
|
|
| 353 |
onClick={handleSubmit}
|
| 354 |
disabled={!name.trim()}
|
| 355 |
+
>
|
| 356 |
+
{initial ? "Save" : "Add"}
|
| 357 |
+
</button>
|
| 358 |
+
</div>
|
| 359 |
+
</div>
|
| 360 |
);
|
| 361 |
}
|
|
@@ -1,5 +1,4 @@
|
|
| 1 |
-
import { useRef, useState,
|
| 2 |
-
import { Box, Typography } from "@mui/material";
|
| 3 |
|
| 4 |
const L = 0.75;
|
| 5 |
const C = 0.12;
|
|
@@ -54,7 +53,6 @@ function getColorName(hue: number): string {
|
|
| 54 |
return best;
|
| 55 |
}
|
| 56 |
|
| 57 |
-
// OKLCH -> sRGB -> hex
|
| 58 |
function oklchToHex(l: number, c: number, h: number): string {
|
| 59 |
const hRad = (h * Math.PI) / 180;
|
| 60 |
const a = c * Math.cos(hRad);
|
|
@@ -115,7 +113,6 @@ export function HueSlider({ hue, onChange }: HueSliderProps) {
|
|
| 115 |
setDragging(false);
|
| 116 |
}, []);
|
| 117 |
|
| 118 |
-
// Keyboard support
|
| 119 |
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
| 120 |
const step = e.shiftKey ? 10 : 2;
|
| 121 |
if (e.key === "ArrowLeft") { e.preventDefault(); onChange(((hue - step) % 360 + 360) % 360); }
|
|
@@ -123,32 +120,29 @@ export function HueSlider({ hue, onChange }: HueSliderProps) {
|
|
| 123 |
}, [hue, onChange]);
|
| 124 |
|
| 125 |
return (
|
| 126 |
-
<
|
| 127 |
-
{
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
sx={{
|
| 131 |
width: 44,
|
| 132 |
height: 44,
|
| 133 |
-
borderRadius:
|
| 134 |
-
|
| 135 |
-
border: "1px solid",
|
| 136 |
-
borderColor: "divider",
|
| 137 |
flexShrink: 0,
|
| 138 |
}}
|
| 139 |
/>
|
| 140 |
-
<
|
| 141 |
-
<
|
| 142 |
{colorName}
|
| 143 |
-
</
|
| 144 |
-
<
|
| 145 |
-
{hexColor.toUpperCase()} - {Math.round(hue)}
|
| 146 |
-
</
|
| 147 |
-
</
|
| 148 |
-
</
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
<Box
|
| 152 |
ref={sliderRef}
|
| 153 |
role="slider"
|
| 154 |
tabIndex={0}
|
|
@@ -160,21 +154,18 @@ export function HueSlider({ hue, onChange }: HueSliderProps) {
|
|
| 160 |
onPointerMove={handlePointerMove}
|
| 161 |
onPointerUp={handlePointerUp}
|
| 162 |
onKeyDown={handleKeyDown}
|
| 163 |
-
|
| 164 |
position: "relative",
|
| 165 |
height: 16,
|
| 166 |
-
borderRadius:
|
| 167 |
-
border: "1px solid",
|
| 168 |
-
borderColor: "divider",
|
| 169 |
background: "linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)",
|
| 170 |
cursor: "ew-resize",
|
| 171 |
touchAction: "none",
|
| 172 |
-
"&:focus-visible": { outline: "2px solid", outlineColor: "primary.main", outlineOffset: 2 },
|
| 173 |
}}
|
| 174 |
>
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
sx={{
|
| 178 |
position: "absolute",
|
| 179 |
top: "50%",
|
| 180 |
left: `${(hue / 360) * 100}%`,
|
|
@@ -183,13 +174,13 @@ export function HueSlider({ hue, onChange }: HueSliderProps) {
|
|
| 183 |
borderRadius: "50%",
|
| 184 |
border: "2px solid #fff",
|
| 185 |
transform: "translate(-50%, -50%)",
|
| 186 |
-
|
| 187 |
boxShadow: "0 0 0 1px rgba(0,0,0,0.15), 0 1px 3px rgba(0,0,0,0.3)",
|
| 188 |
pointerEvents: "none",
|
| 189 |
}}
|
| 190 |
/>
|
| 191 |
-
</
|
| 192 |
-
</
|
| 193 |
);
|
| 194 |
}
|
| 195 |
|
|
|
|
| 1 |
+
import { useRef, useState, useCallback } from "react";
|
|
|
|
| 2 |
|
| 3 |
const L = 0.75;
|
| 4 |
const C = 0.12;
|
|
|
|
| 53 |
return best;
|
| 54 |
}
|
| 55 |
|
|
|
|
| 56 |
function oklchToHex(l: number, c: number, h: number): string {
|
| 57 |
const hRad = (h * Math.PI) / 180;
|
| 58 |
const a = c * Math.cos(hRad);
|
|
|
|
| 113 |
setDragging(false);
|
| 114 |
}, []);
|
| 115 |
|
|
|
|
| 116 |
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
| 117 |
const step = e.shiftKey ? 10 : 2;
|
| 118 |
if (e.key === "ArrowLeft") { e.preventDefault(); onChange(((hue - step) % 360 + 360) % 360); }
|
|
|
|
| 120 |
}, [hue, onChange]);
|
| 121 |
|
| 122 |
return (
|
| 123 |
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
| 124 |
+
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
| 125 |
+
<div
|
| 126 |
+
style={{
|
|
|
|
| 127 |
width: 44,
|
| 128 |
height: 44,
|
| 129 |
+
borderRadius: 8,
|
| 130 |
+
backgroundColor: hexColor,
|
| 131 |
+
border: "1px solid var(--ed-border)",
|
|
|
|
| 132 |
flexShrink: 0,
|
| 133 |
}}
|
| 134 |
/>
|
| 135 |
+
<div style={{ minWidth: 0 }}>
|
| 136 |
+
<div style={{ fontWeight: 700, fontSize: "0.8rem", lineHeight: 1.3, color: "var(--ed-text)" }}>
|
| 137 |
{colorName}
|
| 138 |
+
</div>
|
| 139 |
+
<div style={{ color: "var(--ed-text-secondary)", fontSize: "0.7rem", fontFamily: "monospace" }}>
|
| 140 |
+
{hexColor.toUpperCase()} - {Math.round(hue)}°
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
<div
|
|
|
|
| 146 |
ref={sliderRef}
|
| 147 |
role="slider"
|
| 148 |
tabIndex={0}
|
|
|
|
| 154 |
onPointerMove={handlePointerMove}
|
| 155 |
onPointerUp={handlePointerUp}
|
| 156 |
onKeyDown={handleKeyDown}
|
| 157 |
+
style={{
|
| 158 |
position: "relative",
|
| 159 |
height: 16,
|
| 160 |
+
borderRadius: 10,
|
| 161 |
+
border: "1px solid var(--ed-border)",
|
|
|
|
| 162 |
background: "linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)",
|
| 163 |
cursor: "ew-resize",
|
| 164 |
touchAction: "none",
|
|
|
|
| 165 |
}}
|
| 166 |
>
|
| 167 |
+
<div
|
| 168 |
+
style={{
|
|
|
|
| 169 |
position: "absolute",
|
| 170 |
top: "50%",
|
| 171 |
left: `${(hue / 360) * 100}%`,
|
|
|
|
| 174 |
borderRadius: "50%",
|
| 175 |
border: "2px solid #fff",
|
| 176 |
transform: "translate(-50%, -50%)",
|
| 177 |
+
backgroundColor: hexColor,
|
| 178 |
boxShadow: "0 0 0 1px rgba(0,0,0,0.15), 0 1px 3px rgba(0,0,0,0.3)",
|
| 179 |
pointerEvents: "none",
|
| 180 |
}}
|
| 181 |
/>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
);
|
| 185 |
}
|
| 186 |
|
|
@@ -1,21 +1,9 @@
|
|
| 1 |
-
import {
|
| 2 |
-
|
| 3 |
-
Box,
|
| 4 |
-
Typography,
|
| 5 |
-
TextField,
|
| 6 |
-
Select,
|
| 7 |
-
MenuItem,
|
| 8 |
-
FormControlLabel,
|
| 9 |
-
Switch,
|
| 10 |
-
Divider,
|
| 11 |
-
IconButton,
|
| 12 |
-
} from "@mui/material";
|
| 13 |
-
import CloseIcon from "@mui/icons-material/Close";
|
| 14 |
-
import { useState, useEffect } from "react";
|
| 15 |
import { useFrontmatter } from "./useFrontmatter";
|
| 16 |
import type { FrontmatterStore, FrontmatterData } from "./frontmatter-store";
|
| 17 |
import type * as Y from "yjs";
|
| 18 |
-
import { HueSlider
|
| 19 |
|
| 20 |
interface SettingsDrawerProps {
|
| 21 |
open: boolean;
|
|
@@ -42,183 +30,160 @@ export function SettingsDrawer({ open, onClose, store, settingsMap }: SettingsDr
|
|
| 42 |
return () => settingsMap.unobserve(sync);
|
| 43 |
}, [settingsMap]);
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
if (!data) return null;
|
| 46 |
|
| 47 |
return (
|
| 48 |
-
<
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
<
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
</FieldGroup>
|
| 88 |
-
|
| 89 |
-
{/* Description (SEO) */}
|
| 90 |
-
<FieldGroup label="Description (SEO)">
|
| 91 |
-
<TextField
|
| 92 |
-
size="small"
|
| 93 |
-
fullWidth
|
| 94 |
-
multiline
|
| 95 |
-
minRows={2}
|
| 96 |
-
maxRows={4}
|
| 97 |
-
placeholder="Short description for meta tags..."
|
| 98 |
-
value={data.description}
|
| 99 |
-
onChange={(e) => update("description", e.target.value)}
|
| 100 |
-
/>
|
| 101 |
-
</FieldGroup>
|
| 102 |
-
|
| 103 |
-
{/* Banner */}
|
| 104 |
-
<FieldGroup label="Banner embed">
|
| 105 |
-
<TextField
|
| 106 |
-
size="small"
|
| 107 |
-
fullWidth
|
| 108 |
-
placeholder="banner.html"
|
| 109 |
-
value={data.banner}
|
| 110 |
-
onChange={(e) => update("banner", e.target.value)}
|
| 111 |
-
/>
|
| 112 |
-
</FieldGroup>
|
| 113 |
-
|
| 114 |
-
{/* Citation style (collaborative via Y.Map) */}
|
| 115 |
-
<FieldGroup label="Citation style">
|
| 116 |
-
<Select
|
| 117 |
-
size="small"
|
| 118 |
-
fullWidth
|
| 119 |
-
value={citationStyle}
|
| 120 |
-
onChange={(e) => {
|
| 121 |
-
const val = e.target.value;
|
| 122 |
-
setCitationStyle(val);
|
| 123 |
-
settingsMap?.set("citationStyle", val);
|
| 124 |
-
}}
|
| 125 |
-
>
|
| 126 |
-
<MenuItem value="apa">APA (7th edition)</MenuItem>
|
| 127 |
-
<MenuItem value="ieee">IEEE</MenuItem>
|
| 128 |
-
<MenuItem value="vancouver">Vancouver</MenuItem>
|
| 129 |
-
<MenuItem value="chicago-author-date">Chicago (author-date)</MenuItem>
|
| 130 |
-
<MenuItem value="harvard1">Harvard</MenuItem>
|
| 131 |
-
</Select>
|
| 132 |
-
</FieldGroup>
|
| 133 |
-
|
| 134 |
-
{/* Primary color (hue slider like the template) */}
|
| 135 |
-
<FieldGroup label="Primary color">
|
| 136 |
-
<HueSlider
|
| 137 |
-
hue={hue}
|
| 138 |
-
onChange={(h) => {
|
| 139 |
-
setHue(h);
|
| 140 |
-
settingsMap?.set("primaryHue", h);
|
| 141 |
-
}}
|
| 142 |
-
/>
|
| 143 |
-
</FieldGroup>
|
| 144 |
-
|
| 145 |
-
<Divider />
|
| 146 |
-
|
| 147 |
-
{/* Toggles */}
|
| 148 |
-
<FormControlLabel
|
| 149 |
-
sx={{ ml: 0 }}
|
| 150 |
-
control={
|
| 151 |
-
<Switch
|
| 152 |
-
size="small"
|
| 153 |
-
checked={data.showPdf}
|
| 154 |
-
onChange={(_, v) => update("showPdf", v)}
|
| 155 |
/>
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
/>
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
<TextField
|
| 177 |
-
size="small"
|
| 178 |
-
fullWidth
|
| 179 |
-
multiline
|
| 180 |
-
minRows={2}
|
| 181 |
-
placeholder="e.g. CC BY 4.0 (HTML allowed)"
|
| 182 |
-
value={data.licence}
|
| 183 |
-
onChange={(e) => update("licence", e.target.value)}
|
| 184 |
/>
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
fullWidth
|
| 191 |
-
placeholder="https://..."
|
| 192 |
-
value={data.seoThumbImage}
|
| 193 |
-
onChange={(e) => update("seoThumbImage", e.target.value)}
|
| 194 |
/>
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
|
|
|
| 204 |
/>
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
);
|
| 212 |
}
|
| 213 |
|
| 214 |
function FieldGroup({ label, children }: { label: string; children: React.ReactNode }) {
|
| 215 |
return (
|
| 216 |
-
<
|
| 217 |
-
<
|
| 218 |
-
{label}
|
| 219 |
-
</Typography>
|
| 220 |
{children}
|
| 221 |
-
</
|
| 222 |
);
|
| 223 |
}
|
| 224 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { X } from "lucide-react";
|
| 2 |
+
import { useState, useEffect, useCallback } from "react";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import { useFrontmatter } from "./useFrontmatter";
|
| 4 |
import type { FrontmatterStore, FrontmatterData } from "./frontmatter-store";
|
| 5 |
import type * as Y from "yjs";
|
| 6 |
+
import { HueSlider } from "./HueSlider";
|
| 7 |
|
| 8 |
interface SettingsDrawerProps {
|
| 9 |
open: boolean;
|
|
|
|
| 30 |
return () => settingsMap.unobserve(sync);
|
| 31 |
}, [settingsMap]);
|
| 32 |
|
| 33 |
+
const handleEscape = useCallback((e: KeyboardEvent) => {
|
| 34 |
+
if (e.key === "Escape") onClose();
|
| 35 |
+
}, [onClose]);
|
| 36 |
+
|
| 37 |
+
useEffect(() => {
|
| 38 |
+
if (open) {
|
| 39 |
+
document.addEventListener("keydown", handleEscape);
|
| 40 |
+
return () => document.removeEventListener("keydown", handleEscape);
|
| 41 |
+
}
|
| 42 |
+
}, [open, handleEscape]);
|
| 43 |
+
|
| 44 |
if (!data) return null;
|
| 45 |
|
| 46 |
return (
|
| 47 |
+
<>
|
| 48 |
+
<div className={`drawer-backdrop ${open ? "open" : ""}`} onClick={onClose} />
|
| 49 |
+
<aside className={`drawer-panel ${open ? "open" : ""}`}>
|
| 50 |
+
<div className="settings-drawer">
|
| 51 |
+
<div className="settings-drawer__header">
|
| 52 |
+
<span className="settings-drawer__title">Article settings</span>
|
| 53 |
+
<button className="icon-btn" onClick={onClose} aria-label="Close">
|
| 54 |
+
<X size={16} />
|
| 55 |
+
</button>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<div className="settings-drawer__body">
|
| 59 |
+
<FieldGroup label="Template">
|
| 60 |
+
<select
|
| 61 |
+
className="form-select"
|
| 62 |
+
value={data.template}
|
| 63 |
+
onChange={(e) => update("template", e.target.value as FrontmatterData["template"])}
|
| 64 |
+
>
|
| 65 |
+
<option value="article">Article (full layout)</option>
|
| 66 |
+
<option value="paper">Paper (single column)</option>
|
| 67 |
+
</select>
|
| 68 |
+
</FieldGroup>
|
| 69 |
+
|
| 70 |
+
<FieldGroup label="Description (SEO)">
|
| 71 |
+
<textarea
|
| 72 |
+
className="form-input"
|
| 73 |
+
rows={3}
|
| 74 |
+
placeholder="Short description for meta tags..."
|
| 75 |
+
value={data.description}
|
| 76 |
+
onChange={(e) => update("description", e.target.value)}
|
| 77 |
+
/>
|
| 78 |
+
</FieldGroup>
|
| 79 |
+
|
| 80 |
+
<FieldGroup label="Banner embed">
|
| 81 |
+
<input
|
| 82 |
+
className="form-input"
|
| 83 |
+
placeholder="banner.html"
|
| 84 |
+
value={data.banner}
|
| 85 |
+
onChange={(e) => update("banner", e.target.value)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
/>
|
| 87 |
+
</FieldGroup>
|
| 88 |
+
|
| 89 |
+
<FieldGroup label="Citation style">
|
| 90 |
+
<select
|
| 91 |
+
className="form-select"
|
| 92 |
+
value={citationStyle}
|
| 93 |
+
onChange={(e) => {
|
| 94 |
+
const val = e.target.value;
|
| 95 |
+
setCitationStyle(val);
|
| 96 |
+
settingsMap?.set("citationStyle", val);
|
| 97 |
+
}}
|
| 98 |
+
>
|
| 99 |
+
<option value="apa">APA (7th edition)</option>
|
| 100 |
+
<option value="ieee">IEEE</option>
|
| 101 |
+
<option value="vancouver">Vancouver</option>
|
| 102 |
+
<option value="chicago-author-date">Chicago (author-date)</option>
|
| 103 |
+
<option value="harvard1">Harvard</option>
|
| 104 |
+
</select>
|
| 105 |
+
</FieldGroup>
|
| 106 |
+
|
| 107 |
+
<FieldGroup label="Primary color">
|
| 108 |
+
<HueSlider
|
| 109 |
+
hue={hue}
|
| 110 |
+
onChange={(h) => {
|
| 111 |
+
setHue(h);
|
| 112 |
+
settingsMap?.set("primaryHue", h);
|
| 113 |
+
}}
|
| 114 |
/>
|
| 115 |
+
</FieldGroup>
|
| 116 |
+
|
| 117 |
+
<hr className="divider-h" />
|
| 118 |
+
|
| 119 |
+
<SwitchField
|
| 120 |
+
label="Show PDF download"
|
| 121 |
+
checked={data.showPdf}
|
| 122 |
+
onChange={(v) => update("showPdf", v)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
/>
|
| 124 |
+
|
| 125 |
+
<SwitchField
|
| 126 |
+
label="Auto-collapse TOC"
|
| 127 |
+
checked={data.tableOfContentsAutoCollapse}
|
| 128 |
+
onChange={(v) => update("tableOfContentsAutoCollapse", v)}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
/>
|
| 130 |
+
|
| 131 |
+
<hr className="divider-h" />
|
| 132 |
+
|
| 133 |
+
<FieldGroup label="Licence">
|
| 134 |
+
<textarea
|
| 135 |
+
className="form-input"
|
| 136 |
+
rows={2}
|
| 137 |
+
placeholder="e.g. CC BY 4.0 (HTML allowed)"
|
| 138 |
+
value={data.licence}
|
| 139 |
+
onChange={(e) => update("licence", e.target.value)}
|
| 140 |
/>
|
| 141 |
+
</FieldGroup>
|
| 142 |
+
|
| 143 |
+
<FieldGroup label="SEO thumbnail image">
|
| 144 |
+
<input
|
| 145 |
+
className="form-input"
|
| 146 |
+
placeholder="https://..."
|
| 147 |
+
value={data.seoThumbImage}
|
| 148 |
+
onChange={(e) => update("seoThumbImage", e.target.value)}
|
| 149 |
+
/>
|
| 150 |
+
</FieldGroup>
|
| 151 |
+
|
| 152 |
+
<SwitchField
|
| 153 |
+
label="PDF Pro only"
|
| 154 |
+
checked={data.pdfProOnly}
|
| 155 |
+
onChange={(v) => update("pdfProOnly", v)}
|
| 156 |
+
/>
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
</aside>
|
| 160 |
+
</>
|
| 161 |
);
|
| 162 |
}
|
| 163 |
|
| 164 |
function FieldGroup({ label, children }: { label: string; children: React.ReactNode }) {
|
| 165 |
return (
|
| 166 |
+
<div>
|
| 167 |
+
<label className="field-label">{label}</label>
|
|
|
|
|
|
|
| 168 |
{children}
|
| 169 |
+
</div>
|
| 170 |
);
|
| 171 |
}
|
| 172 |
|
| 173 |
+
function SwitchField({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }) {
|
| 174 |
+
return (
|
| 175 |
+
<label className="form-switch-label">
|
| 176 |
+
<span className="form-switch">
|
| 177 |
+
<input
|
| 178 |
+
type="checkbox"
|
| 179 |
+
role="switch"
|
| 180 |
+
checked={checked}
|
| 181 |
+
onChange={(e) => onChange(e.target.checked)}
|
| 182 |
+
/>
|
| 183 |
+
<span className="form-switch__track" />
|
| 184 |
+
<span className="form-switch__thumb" />
|
| 185 |
+
</span>
|
| 186 |
+
{label}
|
| 187 |
+
</label>
|
| 188 |
+
);
|
| 189 |
+
}
|
|
@@ -1,6 +1,5 @@
|
|
| 1 |
import React from "react";
|
| 2 |
import ReactDOM from "react-dom/client";
|
| 3 |
-
import { CssBaseline } from "@mui/material";
|
| 4 |
import App from "./App";
|
| 5 |
|
| 6 |
// Template foundation (source of truth - same as research-article-template)
|
|
@@ -16,9 +15,11 @@ import "./styles/components/_card.css";
|
|
| 16 |
import "./styles/components/_mermaid.css";
|
| 17 |
import "./styles/components/_hero.css";
|
| 18 |
import "./styles/components/_toc.css";
|
| 19 |
-
//
|
| 20 |
-
//
|
| 21 |
-
|
|
|
|
|
|
|
| 22 |
|
| 23 |
// Editor extensions
|
| 24 |
import "./styles/_editor-tokens.css";
|
|
@@ -28,7 +29,6 @@ import "./styles/editing.css";
|
|
| 28 |
|
| 29 |
ReactDOM.createRoot(document.getElementById("root")!).render(
|
| 30 |
<React.StrictMode>
|
| 31 |
-
<CssBaseline />
|
| 32 |
<App />
|
| 33 |
</React.StrictMode>
|
| 34 |
);
|
|
|
|
| 1 |
import React from "react";
|
| 2 |
import ReactDOM from "react-dom/client";
|
|
|
|
| 3 |
import App from "./App";
|
| 4 |
|
| 5 |
// Template foundation (source of truth - same as research-article-template)
|
|
|
|
| 15 |
import "./styles/components/_mermaid.css";
|
| 16 |
import "./styles/components/_hero.css";
|
| 17 |
import "./styles/components/_toc.css";
|
| 18 |
+
import "./styles/components/_button.css";
|
| 19 |
+
import "./styles/components/_form.css";
|
| 20 |
+
|
| 21 |
+
// Editor chrome UI
|
| 22 |
+
import "./styles/_ui.css";
|
| 23 |
|
| 24 |
// Editor extensions
|
| 25 |
import "./styles/_editor-tokens.css";
|
|
|
|
| 29 |
|
| 30 |
ReactDOM.createRoot(document.getElementById("root")!).render(
|
| 31 |
<React.StrictMode>
|
|
|
|
| 32 |
<App />
|
| 33 |
</React.StrictMode>
|
| 34 |
);
|
|
@@ -1,14 +1,6 @@
|
|
| 1 |
@import "https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,200..900;1,200..900&display=swap";
|
| 2 |
|
| 3 |
-
|
| 4 |
-
* Template originally sets html { font-size; line-height; background-color;
|
| 5 |
-
* overflow-x }. In the editor we scope font-size/line-height to the article
|
| 6 |
-
* area so MUI portals (Dialog, Menu, Tooltip) keep their own defaults.
|
| 7 |
-
*/
|
| 8 |
-
|
| 9 |
-
.content-grid,
|
| 10 |
-
.hero,
|
| 11 |
-
.meta {
|
| 12 |
font-size: 16px;
|
| 13 |
line-height: 1.6;
|
| 14 |
}
|
|
|
|
| 1 |
@import "https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,200..900;1,200..900&display=swap";
|
| 2 |
|
| 3 |
+
html {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
font-size: 16px;
|
| 5 |
line-height: 1.6;
|
| 6 |
}
|
|
@@ -0,0 +1,397 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================================ */
|
| 2 |
+
/* Publisher-only CSS overrides */
|
| 3 |
+
/* Injected into the published static HTML. Not used in the editor. */
|
| 4 |
+
/* ============================================================================ */
|
| 5 |
+
|
| 6 |
+
/* Published page global reset */
|
| 7 |
+
body { font-family: var(--default-font-family); color: var(--text-color); }
|
| 8 |
+
html { font-size: 16px; line-height: 1.6; background-color: var(--page-bg); overflow-x: hidden; scroll-behavior: smooth; scroll-padding-top: 80px; }
|
| 9 |
+
|
| 10 |
+
/* Theme toggle icon wrapper (from ThemeToggle.astro) */
|
| 11 |
+
#theme-toggle .icon-wrapper {
|
| 12 |
+
display: grid;
|
| 13 |
+
place-items: center;
|
| 14 |
+
width: 20px;
|
| 15 |
+
height: 20px;
|
| 16 |
+
}
|
| 17 |
+
#theme-toggle .icon-wrapper .icon {
|
| 18 |
+
grid-area: 1 / 1;
|
| 19 |
+
filter: none !important;
|
| 20 |
+
}
|
| 21 |
+
#theme-toggle .icon-wrapper.animated .icon {
|
| 22 |
+
transition: opacity 0.35s ease;
|
| 23 |
+
}
|
| 24 |
+
#theme-toggle .icon-wrapper.spin-cw { animation: spin-cw 0.5s cubic-bezier(0.4,0,0.2,1); }
|
| 25 |
+
#theme-toggle .icon-wrapper.spin-ccw { animation: spin-ccw 0.5s cubic-bezier(0.4,0,0.2,1); }
|
| 26 |
+
@keyframes spin-cw { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
| 27 |
+
@keyframes spin-ccw { from { transform: rotate(0deg); } to { transform: rotate(-360deg); } }
|
| 28 |
+
|
| 29 |
+
/* Note component - aligned with template Note.astro */
|
| 30 |
+
div[data-component="note"] {
|
| 31 |
+
background: var(--surface-bg);
|
| 32 |
+
border-left: 2px solid var(--border-color);
|
| 33 |
+
border-top-right-radius: 8px;
|
| 34 |
+
border-bottom-right-radius: 8px;
|
| 35 |
+
padding: 20px 18px;
|
| 36 |
+
margin: var(--block-spacing-y, 1em) 0;
|
| 37 |
+
}
|
| 38 |
+
div[data-component="note"][variant="info"] {
|
| 39 |
+
border-left-color: #f39c12;
|
| 40 |
+
background: color-mix(in oklab, #f39c12 10%, var(--surface-bg));
|
| 41 |
+
}
|
| 42 |
+
div[data-component="note"][variant="success"] {
|
| 43 |
+
border-left-color: #2ecc71;
|
| 44 |
+
background: color-mix(in oklab, #2ecc71 8%, var(--surface-bg));
|
| 45 |
+
}
|
| 46 |
+
div[data-component="note"][variant="danger"] {
|
| 47 |
+
border-left-color: #e74c3c;
|
| 48 |
+
background: color-mix(in oklab, #e74c3c 8%, var(--surface-bg));
|
| 49 |
+
}
|
| 50 |
+
div[data-component="note"] .note__title {
|
| 51 |
+
font-size: 16px;
|
| 52 |
+
font-weight: 600;
|
| 53 |
+
color: var(--text-color);
|
| 54 |
+
margin-bottom: 6px;
|
| 55 |
+
}
|
| 56 |
+
div[data-component="note"] > *:first-child { margin-top: 0; }
|
| 57 |
+
div[data-component="note"] > *:last-child { margin-bottom: 0; }
|
| 58 |
+
|
| 59 |
+
/* Quote component - aligned with template Quote.astro */
|
| 60 |
+
div[data-component="quoteBlock"] {
|
| 61 |
+
position: relative;
|
| 62 |
+
margin: 32px 0;
|
| 63 |
+
max-width: 600px;
|
| 64 |
+
padding: 0;
|
| 65 |
+
border: none;
|
| 66 |
+
background: none;
|
| 67 |
+
}
|
| 68 |
+
div[data-component="quoteBlock"]::before {
|
| 69 |
+
content: '\201C';
|
| 70 |
+
position: absolute;
|
| 71 |
+
top: -24px;
|
| 72 |
+
left: -30px;
|
| 73 |
+
font-size: 8rem;
|
| 74 |
+
font-weight: 400;
|
| 75 |
+
color: var(--text-color);
|
| 76 |
+
opacity: 0.05;
|
| 77 |
+
z-index: -1;
|
| 78 |
+
line-height: 1;
|
| 79 |
+
pointer-events: none;
|
| 80 |
+
}
|
| 81 |
+
div[data-component="quoteBlock"] .quote-text {
|
| 82 |
+
font-size: 1.5rem;
|
| 83 |
+
line-height: 1.4;
|
| 84 |
+
font-weight: 400;
|
| 85 |
+
letter-spacing: -0.01em;
|
| 86 |
+
color: var(--text-color);
|
| 87 |
+
}
|
| 88 |
+
div[data-component="quoteBlock"] .quote-footer {
|
| 89 |
+
font-size: 0.875rem;
|
| 90 |
+
color: var(--muted-color);
|
| 91 |
+
margin-top: 12px;
|
| 92 |
+
}
|
| 93 |
+
div[data-component="quoteBlock"] .quote-author::before {
|
| 94 |
+
content: '\2014\00A0';
|
| 95 |
+
font-style: normal;
|
| 96 |
+
}
|
| 97 |
+
div[data-component="quoteBlock"] .quote-author {
|
| 98 |
+
font-weight: 500;
|
| 99 |
+
font-style: italic;
|
| 100 |
+
color: var(--text-color);
|
| 101 |
+
opacity: 0.85;
|
| 102 |
+
}
|
| 103 |
+
div[data-component="quoteBlock"] .quote-source {
|
| 104 |
+
font-weight: 500;
|
| 105 |
+
font-style: italic;
|
| 106 |
+
color: var(--text-color);
|
| 107 |
+
opacity: 0.85;
|
| 108 |
+
}
|
| 109 |
+
div[data-component="quoteBlock"] > *:first-child { margin-top: 0; }
|
| 110 |
+
div[data-component="quoteBlock"] > *:last-child { margin-bottom: 0; }
|
| 111 |
+
|
| 112 |
+
/* Sidenote component - aligned with template Sidenote.astro */
|
| 113 |
+
div[data-component="sidenote"] {
|
| 114 |
+
font-size: 0.9rem;
|
| 115 |
+
color: var(--muted-color);
|
| 116 |
+
padding: 0 30px;
|
| 117 |
+
margin: 1em 0;
|
| 118 |
+
}
|
| 119 |
+
div[data-component="sidenote"] > *:first-child { margin-top: 0; }
|
| 120 |
+
div[data-component="sidenote"] > *:last-child { margin-bottom: 0; }
|
| 121 |
+
|
| 122 |
+
/* Reference wrapper - aligned with template Reference.astro */
|
| 123 |
+
div[data-component="reference"] {
|
| 124 |
+
margin: 0 0 var(--spacing-4, 1rem);
|
| 125 |
+
}
|
| 126 |
+
div[data-component="reference"] .reference__caption {
|
| 127 |
+
text-align: left;
|
| 128 |
+
font-size: 0.9rem;
|
| 129 |
+
color: var(--muted-color);
|
| 130 |
+
margin-top: 6px;
|
| 131 |
+
}
|
| 132 |
+
div[data-component="reference"] > *:first-child { margin-top: 0; }
|
| 133 |
+
div[data-component="reference"] > *:last-child { margin-bottom: 0; }
|
| 134 |
+
|
| 135 |
+
/* Stack / multi-column layout - aligned with template Stack.astro */
|
| 136 |
+
div[data-component="stack"] {
|
| 137 |
+
display: grid;
|
| 138 |
+
gap: 1rem;
|
| 139 |
+
margin: var(--block-spacing-y, 1.5em) 0;
|
| 140 |
+
width: 100%;
|
| 141 |
+
max-width: 100%;
|
| 142 |
+
box-sizing: border-box;
|
| 143 |
+
}
|
| 144 |
+
div[data-component="stack"][data-layout="2-column"] { grid-template-columns: repeat(2, 1fr); }
|
| 145 |
+
div[data-component="stack"][data-layout="3-column"] { grid-template-columns: repeat(3, 1fr); }
|
| 146 |
+
div[data-component="stack"][data-layout="4-column"] { grid-template-columns: repeat(4, 1fr); }
|
| 147 |
+
div[data-component="stack"][data-layout="auto"] { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
|
| 148 |
+
div[data-component="stack"]:not([data-layout]) { grid-template-columns: repeat(2, 1fr); }
|
| 149 |
+
div[data-component="stack"][data-gap="small"] { gap: 0.5rem; }
|
| 150 |
+
div[data-component="stack"][data-gap="large"] { gap: 2rem; }
|
| 151 |
+
div[data-type="stack-column"] { min-width: 0; overflow: hidden; word-wrap: break-word; overflow-wrap: break-word; }
|
| 152 |
+
div[data-type="stack-column"] > *:first-child { margin-top: 0; }
|
| 153 |
+
div[data-type="stack-column"] > *:last-child { margin-bottom: 0; }
|
| 154 |
+
@media (max-width: 768px) {
|
| 155 |
+
div[data-component="stack"][data-layout="2-column"],
|
| 156 |
+
div[data-component="stack"][data-layout="3-column"],
|
| 157 |
+
div[data-component="stack"][data-layout="4-column"],
|
| 158 |
+
div[data-component="stack"][data-layout="auto"],
|
| 159 |
+
div[data-component="stack"]:not([data-layout]) { grid-template-columns: 1fr !important; }
|
| 160 |
+
}
|
| 161 |
+
@media (min-width: 769px) and (max-width: 1100px) {
|
| 162 |
+
div[data-component="stack"][data-layout="3-column"],
|
| 163 |
+
div[data-component="stack"][data-layout="4-column"],
|
| 164 |
+
div[data-component="stack"][data-layout="auto"] { grid-template-columns: repeat(2, 1fr) !important; }
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
/* Wide / full-width helpers */
|
| 168 |
+
div[data-component="wide"] {
|
| 169 |
+
width: min(1100px, 100vw - 64px);
|
| 170 |
+
margin-left: 50%;
|
| 171 |
+
transform: translateX(-50%);
|
| 172 |
+
}
|
| 173 |
+
div[data-component="fullWidth"] {
|
| 174 |
+
width: 100vw;
|
| 175 |
+
margin-left: calc(50% - 50vw);
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
/* Accordion - aligned with template Accordion.astro */
|
| 179 |
+
details[data-component="accordion"] {
|
| 180 |
+
border: 1px solid var(--border-color);
|
| 181 |
+
border-radius: var(--table-border-radius, 8px);
|
| 182 |
+
background: var(--surface-bg);
|
| 183 |
+
padding: 0;
|
| 184 |
+
margin: 0 0 var(--spacing-4, 1em);
|
| 185 |
+
transition: box-shadow 180ms ease, border-color 180ms ease;
|
| 186 |
+
}
|
| 187 |
+
details[data-component="accordion"][open] {
|
| 188 |
+
border-color: color-mix(in oklab, var(--border-color), var(--primary-color) 20%);
|
| 189 |
+
}
|
| 190 |
+
details[data-component="accordion"] > summary {
|
| 191 |
+
display: flex;
|
| 192 |
+
align-items: center;
|
| 193 |
+
justify-content: space-between;
|
| 194 |
+
gap: 4px;
|
| 195 |
+
padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);
|
| 196 |
+
cursor: pointer;
|
| 197 |
+
font-weight: 600;
|
| 198 |
+
color: var(--text-color);
|
| 199 |
+
list-style: none;
|
| 200 |
+
user-select: none;
|
| 201 |
+
}
|
| 202 |
+
details[data-component="accordion"][open] > summary {
|
| 203 |
+
border-bottom: 1px solid var(--border-color);
|
| 204 |
+
}
|
| 205 |
+
details[data-component="accordion"] > summary::-webkit-details-marker { display: none; }
|
| 206 |
+
details[data-component="accordion"] > summary::marker { content: ""; }
|
| 207 |
+
details[data-component="accordion"] > summary::after {
|
| 208 |
+
content: '';
|
| 209 |
+
display: inline-block;
|
| 210 |
+
width: 0.5em;
|
| 211 |
+
height: 0.5em;
|
| 212 |
+
border-right: 2px solid currentColor;
|
| 213 |
+
border-bottom: 2px solid currentColor;
|
| 214 |
+
transform: rotate(-45deg);
|
| 215 |
+
transition: transform 220ms ease;
|
| 216 |
+
opacity: 0.6;
|
| 217 |
+
flex-shrink: 0;
|
| 218 |
+
}
|
| 219 |
+
details[data-component="accordion"][open] > summary::after {
|
| 220 |
+
transform: rotate(45deg);
|
| 221 |
+
}
|
| 222 |
+
details[data-component="accordion"] > .accordion-content {
|
| 223 |
+
padding: 8px;
|
| 224 |
+
}
|
| 225 |
+
details[data-component="accordion"] > .accordion-content > *:first-child { margin-top: 0; }
|
| 226 |
+
details[data-component="accordion"] > .accordion-content > *:last-child { margin-bottom: 0; }
|
| 227 |
+
|
| 228 |
+
/* Footer */
|
| 229 |
+
.footer {
|
| 230 |
+
contain: layout style;
|
| 231 |
+
font-size: 0.8em;
|
| 232 |
+
line-height: 1.7em;
|
| 233 |
+
margin-top: 60px;
|
| 234 |
+
margin-bottom: 0;
|
| 235 |
+
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
| 236 |
+
color: rgba(0, 0, 0, 0.5);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.footer-inner {
|
| 240 |
+
max-width: 1280px;
|
| 241 |
+
margin: 0 auto;
|
| 242 |
+
padding: 60px 16px 48px;
|
| 243 |
+
display: grid;
|
| 244 |
+
grid-template-columns: 220px minmax(0, 680px) 260px;
|
| 245 |
+
gap: 32px;
|
| 246 |
+
align-items: start;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.citation-block,
|
| 250 |
+
.references-block,
|
| 251 |
+
.reuse-block,
|
| 252 |
+
.doi-block {
|
| 253 |
+
display: contents;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.citation-block > .footer-heading,
|
| 257 |
+
.references-block > .footer-heading,
|
| 258 |
+
.reuse-block > .footer-heading,
|
| 259 |
+
.doi-block > .footer-heading {
|
| 260 |
+
grid-column: 1;
|
| 261 |
+
font-size: 15px;
|
| 262 |
+
font-weight: 600;
|
| 263 |
+
margin: 0;
|
| 264 |
+
text-align: right;
|
| 265 |
+
padding-right: 30px;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.citation-block > :not(.footer-heading),
|
| 269 |
+
.references-block > :not(.footer-heading),
|
| 270 |
+
.reuse-block > :not(.footer-heading),
|
| 271 |
+
.doi-block > :not(.footer-heading) {
|
| 272 |
+
grid-column: 2;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.citation-block .footer-heading { margin: 0 0 8px; }
|
| 276 |
+
|
| 277 |
+
.citation-block p,
|
| 278 |
+
.reuse-block p,
|
| 279 |
+
.doi-block p,
|
| 280 |
+
.footnotes ol,
|
| 281 |
+
.footnotes ol p,
|
| 282 |
+
.references { margin-top: 0; }
|
| 283 |
+
|
| 284 |
+
.footnote-ref a {
|
| 285 |
+
color: var(--primary-color, #958DF1);
|
| 286 |
+
text-decoration: none;
|
| 287 |
+
font-size: 0.8em;
|
| 288 |
+
}
|
| 289 |
+
.footnote-ref a:hover { text-decoration: underline; }
|
| 290 |
+
.footnotes ol { padding-left: 1.5em; }
|
| 291 |
+
.footnotes li { margin-bottom: 0.5em; font-size: 0.9rem; color: var(--muted-color, #888); }
|
| 292 |
+
.footnotes li p { margin: 0; }
|
| 293 |
+
.footnote-backref { text-decoration: none; margin-left: 4px; }
|
| 294 |
+
|
| 295 |
+
.citation {
|
| 296 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 297 |
+
font-size: 11px;
|
| 298 |
+
line-height: 15px;
|
| 299 |
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
| 300 |
+
background: rgba(0, 0, 0, 0.02);
|
| 301 |
+
padding: 10px 18px;
|
| 302 |
+
border-radius: 3px;
|
| 303 |
+
color: rgba(150, 150, 150, 1);
|
| 304 |
+
overflow: hidden;
|
| 305 |
+
margin-top: -12px;
|
| 306 |
+
white-space: pre-wrap;
|
| 307 |
+
word-wrap: break-word;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.citation a { color: rgba(0, 0, 0, 0.6); text-decoration: underline; }
|
| 311 |
+
.citation.short { margin-top: -4px; }
|
| 312 |
+
|
| 313 |
+
.citation-inline {
|
| 314 |
+
color: var(--primary-color, #958df1);
|
| 315 |
+
text-decoration: none;
|
| 316 |
+
text-decoration-line: underline;
|
| 317 |
+
text-decoration-color: transparent;
|
| 318 |
+
text-underline-offset: 2px;
|
| 319 |
+
cursor: pointer;
|
| 320 |
+
transition: text-decoration-color 0.15s;
|
| 321 |
+
}
|
| 322 |
+
.citation-inline:hover {
|
| 323 |
+
text-decoration-color: currentColor;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.bibliography-content .csl-entry {
|
| 327 |
+
margin-bottom: 0.75em;
|
| 328 |
+
padding-left: 1.5em;
|
| 329 |
+
text-indent: -1.5em;
|
| 330 |
+
font-size: 0.9em;
|
| 331 |
+
line-height: 1.5;
|
| 332 |
+
}
|
| 333 |
+
.bibliography-content .csl-entry:target {
|
| 334 |
+
background: rgba(149, 141, 241, 0.1);
|
| 335 |
+
border-radius: 4px;
|
| 336 |
+
padding: 4px 4px 4px 1.5em;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.references-block .footer-heading { margin: 0; }
|
| 340 |
+
.references-block ol { padding: 0 0 0 15px; }
|
| 341 |
+
.references-block li { margin-bottom: 1em; }
|
| 342 |
+
.references-block a { color: var(--text-color); }
|
| 343 |
+
|
| 344 |
+
.footer a {
|
| 345 |
+
color: var(--primary-color);
|
| 346 |
+
border-bottom: 1px solid var(--link-underline);
|
| 347 |
+
text-decoration: none;
|
| 348 |
+
}
|
| 349 |
+
.footer a:hover {
|
| 350 |
+
color: var(--primary-color-hover);
|
| 351 |
+
border-bottom-color: var(--link-underline-hover);
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.template-credit { display: contents; }
|
| 355 |
+
.template-credit p {
|
| 356 |
+
grid-column: 2;
|
| 357 |
+
margin: 24px 0 0 0;
|
| 358 |
+
font-size: 0.85em;
|
| 359 |
+
color: rgba(0, 0, 0, 0.5);
|
| 360 |
+
}
|
| 361 |
+
.template-credit a { color: rgba(0, 0, 0, 0.6); border-bottom: 1px solid rgba(0, 0, 0, 0.15); }
|
| 362 |
+
.template-credit a:hover { color: rgba(0, 0, 0, 0.8); border-bottom-color: rgba(0, 0, 0, 0.3); }
|
| 363 |
+
|
| 364 |
+
[data-theme="dark"] .footer { border-top-color: rgba(255, 255, 255, 0.15); color: rgba(200, 200, 200, 0.8); }
|
| 365 |
+
[data-theme="dark"] .citation { background: rgba(255, 255, 255, 0.04); border-color: rgba(255, 255, 255, 0.15); color: rgba(200, 200, 200, 1); }
|
| 366 |
+
[data-theme="dark"] .citation a { color: rgba(255, 255, 255, 0.75); }
|
| 367 |
+
[data-theme="dark"] .bibliography-content .csl-entry:target { background: rgba(149, 141, 241, 0.15); }
|
| 368 |
+
[data-theme="dark"] .footer a { color: var(--primary-color); }
|
| 369 |
+
[data-theme="dark"] .template-credit p { color: rgba(200, 200, 200, 0.6); }
|
| 370 |
+
[data-theme="dark"] .template-credit a { color: rgba(200, 200, 200, 0.7); border-bottom-color: rgba(255, 255, 255, 0.2); }
|
| 371 |
+
[data-theme="dark"] .template-credit a:hover { color: rgba(200, 200, 200, 0.9); border-bottom-color: rgba(255, 255, 255, 0.35); }
|
| 372 |
+
|
| 373 |
+
@media (max-width: 1100px) {
|
| 374 |
+
.footer-inner { grid-template-columns: 1fr; gap: 16px; display: block; padding: 40px 16px; }
|
| 375 |
+
.footer-inner > .footer-heading { grid-column: auto; margin-top: 16px; }
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
@media (min-width: 768px) {
|
| 379 |
+
.references-block ol { padding: 0 0 0 30px; margin-left: -30px; }
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
/* PDF download link */
|
| 383 |
+
a.pdf-link {
|
| 384 |
+
display: inline-flex;
|
| 385 |
+
align-items: center;
|
| 386 |
+
gap: 0.4em;
|
| 387 |
+
color: var(--accent-color, #4493f8);
|
| 388 |
+
text-decoration: none;
|
| 389 |
+
font-weight: 500;
|
| 390 |
+
}
|
| 391 |
+
a.pdf-link:hover { text-decoration: underline; }
|
| 392 |
+
a.pdf-link::before { content: "\1F4C4"; }
|
| 393 |
+
|
| 394 |
+
/* Image lightbox dialog */
|
| 395 |
+
dialog.lightbox { border: none; background: transparent; padding: 0; max-width: 95vw; max-height: 95vh; }
|
| 396 |
+
dialog.lightbox::backdrop { background: rgba(0, 0, 0, 0.85); }
|
| 397 |
+
dialog.lightbox img { max-width: 95vw; max-height: 90vh; object-fit: contain; border-radius: 4px; }
|
|
@@ -1,6 +1,10 @@
|
|
| 1 |
html { box-sizing: border-box; }
|
| 2 |
*, *::before, *::after { box-sizing: inherit; }
|
| 3 |
-
body {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
audio { display: block; width: 100%; }
|
| 5 |
|
| 6 |
img,
|
|
@@ -11,15 +15,3 @@ picture {
|
|
| 11 |
position: relative;
|
| 12 |
z-index: var(--z-elevated);
|
| 13 |
}
|
| 14 |
-
|
| 15 |
-
/*
|
| 16 |
-
* Article-specific font and color are scoped to the article area
|
| 17 |
-
* to avoid overriding MUI theme on portals (Dialog, Menu, Tooltip).
|
| 18 |
-
* The template's original `body { font-family; color }` is moved here.
|
| 19 |
-
*/
|
| 20 |
-
.content-grid,
|
| 21 |
-
.hero,
|
| 22 |
-
.meta {
|
| 23 |
-
font-family: var(--default-font-family);
|
| 24 |
-
color: var(--text-color);
|
| 25 |
-
}
|
|
|
|
| 1 |
html { box-sizing: border-box; }
|
| 2 |
*, *::before, *::after { box-sizing: inherit; }
|
| 3 |
+
body {
|
| 4 |
+
margin: 0;
|
| 5 |
+
font-family: var(--default-font-family);
|
| 6 |
+
color: var(--text-color);
|
| 7 |
+
}
|
| 8 |
audio { display: block; width: 100%; }
|
| 9 |
|
| 10 |
img,
|
|
|
|
| 15 |
position: relative;
|
| 16 |
z-index: var(--z-elevated);
|
| 17 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -0,0 +1,554 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================================ */
|
| 2 |
+
/* Editor Chrome UI Components */
|
| 3 |
+
/* Replaces MUI: always-dark editor shell using CSS custom properties. */
|
| 4 |
+
/* ============================================================================ */
|
| 5 |
+
|
| 6 |
+
:root {
|
| 7 |
+
--ed-bg: #0f0f0f;
|
| 8 |
+
--ed-surface: #1a1a1a;
|
| 9 |
+
--ed-surface-hover: #222;
|
| 10 |
+
--ed-text: #e0e0e0;
|
| 11 |
+
--ed-text-secondary: #888;
|
| 12 |
+
--ed-text-disabled: #555;
|
| 13 |
+
--ed-border: #2a2a2a;
|
| 14 |
+
--ed-radius: 8px;
|
| 15 |
+
--ed-radius-sm: 6px;
|
| 16 |
+
--ed-success: #66bb6a;
|
| 17 |
+
--ed-error: #f44336;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/* ---- Icon button ---- */
|
| 21 |
+
.icon-btn {
|
| 22 |
+
display: inline-flex;
|
| 23 |
+
align-items: center;
|
| 24 |
+
justify-content: center;
|
| 25 |
+
border: none;
|
| 26 |
+
background: none;
|
| 27 |
+
cursor: pointer;
|
| 28 |
+
padding: 6px;
|
| 29 |
+
border-radius: var(--ed-radius-sm);
|
| 30 |
+
color: var(--ed-text-disabled);
|
| 31 |
+
transition: color 0.15s, background-color 0.15s;
|
| 32 |
+
line-height: 0;
|
| 33 |
+
}
|
| 34 |
+
.icon-btn:hover {
|
| 35 |
+
color: var(--ed-text);
|
| 36 |
+
background: rgba(255, 255, 255, 0.08);
|
| 37 |
+
}
|
| 38 |
+
.icon-btn:focus-visible {
|
| 39 |
+
outline: 2px solid var(--primary-color);
|
| 40 |
+
outline-offset: 2px;
|
| 41 |
+
}
|
| 42 |
+
.icon-btn svg { width: 18px; height: 18px; }
|
| 43 |
+
.icon-btn--active { color: var(--primary-color); }
|
| 44 |
+
.icon-btn--primary { color: var(--primary-color); }
|
| 45 |
+
|
| 46 |
+
/* ---- Button ---- */
|
| 47 |
+
.btn {
|
| 48 |
+
display: inline-flex;
|
| 49 |
+
align-items: center;
|
| 50 |
+
justify-content: center;
|
| 51 |
+
gap: 6px;
|
| 52 |
+
border: none;
|
| 53 |
+
background: none;
|
| 54 |
+
cursor: pointer;
|
| 55 |
+
padding: 6px 14px;
|
| 56 |
+
border-radius: var(--ed-radius-sm);
|
| 57 |
+
font-size: 0.85rem;
|
| 58 |
+
font-weight: 500;
|
| 59 |
+
font-family: inherit;
|
| 60 |
+
color: var(--ed-text-secondary);
|
| 61 |
+
transition: all 0.15s;
|
| 62 |
+
}
|
| 63 |
+
.btn:hover { color: var(--ed-text); background: rgba(255, 255, 255, 0.06); }
|
| 64 |
+
.btn:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; }
|
| 65 |
+
.btn--primary { background: var(--primary-color); color: #000; font-weight: 600; }
|
| 66 |
+
.btn--primary:hover { background: var(--primary-color-hover); color: #000; }
|
| 67 |
+
.btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
| 68 |
+
|
| 69 |
+
/* ---- Chip ---- */
|
| 70 |
+
.chip {
|
| 71 |
+
display: inline-flex;
|
| 72 |
+
align-items: center;
|
| 73 |
+
gap: 4px;
|
| 74 |
+
padding: 2px 8px;
|
| 75 |
+
border-radius: 999px;
|
| 76 |
+
font-size: 0.65rem;
|
| 77 |
+
font-weight: 600;
|
| 78 |
+
line-height: 1;
|
| 79 |
+
height: 22px;
|
| 80 |
+
white-space: nowrap;
|
| 81 |
+
}
|
| 82 |
+
.chip--sm { height: 18px; padding: 1px 6px; }
|
| 83 |
+
.chip--outlined { border: 1px solid var(--ed-border); background: transparent; }
|
| 84 |
+
.chip--clickable { cursor: pointer; }
|
| 85 |
+
.chip--clickable:hover { filter: brightness(1.1); }
|
| 86 |
+
.chip img { width: 18px; height: 18px; border-radius: 50%; object-fit: cover; }
|
| 87 |
+
|
| 88 |
+
/* ---- Surface (replaces MUI Paper) ---- */
|
| 89 |
+
.surface {
|
| 90 |
+
background: var(--ed-surface);
|
| 91 |
+
border: 1px solid var(--ed-border);
|
| 92 |
+
border-radius: var(--ed-radius);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/* ---- Dividers ---- */
|
| 96 |
+
.divider-v { width: 1px; height: 16px; background: var(--ed-border); margin: 0 4px; flex-shrink: 0; }
|
| 97 |
+
.divider-h { border: none; height: 1px; background: var(--ed-border); margin: 0; }
|
| 98 |
+
|
| 99 |
+
/* ---- Spinner ---- */
|
| 100 |
+
.spinner {
|
| 101 |
+
display: inline-block;
|
| 102 |
+
width: 24px;
|
| 103 |
+
height: 24px;
|
| 104 |
+
border: 2.5px solid rgba(255, 255, 255, 0.15);
|
| 105 |
+
border-top-color: var(--primary-color);
|
| 106 |
+
border-radius: 50%;
|
| 107 |
+
animation: ed-spin 0.8s linear infinite;
|
| 108 |
+
}
|
| 109 |
+
@keyframes ed-spin { to { transform: rotate(360deg); } }
|
| 110 |
+
|
| 111 |
+
/* ---- Badge dot ---- */
|
| 112 |
+
.badge-dot { position: relative; }
|
| 113 |
+
.badge-dot::after {
|
| 114 |
+
content: '';
|
| 115 |
+
position: absolute;
|
| 116 |
+
top: 2px;
|
| 117 |
+
right: 2px;
|
| 118 |
+
width: 8px;
|
| 119 |
+
height: 8px;
|
| 120 |
+
background: var(--ed-error);
|
| 121 |
+
border-radius: 50%;
|
| 122 |
+
pointer-events: none;
|
| 123 |
+
}
|
| 124 |
+
.badge-dot--hidden::after { display: none; }
|
| 125 |
+
|
| 126 |
+
/* ---- Native dialog ---- */
|
| 127 |
+
dialog.ed-dialog {
|
| 128 |
+
border: 1px solid var(--ed-border);
|
| 129 |
+
border-radius: var(--ed-radius);
|
| 130 |
+
background: var(--ed-surface);
|
| 131 |
+
color: var(--ed-text);
|
| 132 |
+
padding: 0;
|
| 133 |
+
max-width: 400px;
|
| 134 |
+
width: 90vw;
|
| 135 |
+
}
|
| 136 |
+
dialog.ed-dialog::backdrop { background: rgba(0, 0, 0, 0.6); }
|
| 137 |
+
.ed-dialog__title { padding: 16px 20px 8px; font-size: 1.1rem; font-weight: 600; margin: 0; }
|
| 138 |
+
.ed-dialog__body { padding: 8px 20px 16px; font-size: 0.9rem; color: var(--ed-text-secondary); line-height: 1.6; }
|
| 139 |
+
.ed-dialog__body p { margin: 0; }
|
| 140 |
+
.ed-dialog__actions { display: flex; justify-content: flex-end; gap: 8px; padding: 8px 20px 16px; }
|
| 141 |
+
|
| 142 |
+
/* ---- Drawer ---- */
|
| 143 |
+
.drawer-backdrop {
|
| 144 |
+
position: fixed;
|
| 145 |
+
inset: 0;
|
| 146 |
+
background: rgba(0, 0, 0, 0.5);
|
| 147 |
+
z-index: var(--z-overlay, 1000);
|
| 148 |
+
opacity: 0;
|
| 149 |
+
pointer-events: none;
|
| 150 |
+
transition: opacity 0.25s;
|
| 151 |
+
}
|
| 152 |
+
.drawer-backdrop.open { opacity: 1; pointer-events: auto; }
|
| 153 |
+
|
| 154 |
+
.drawer-panel {
|
| 155 |
+
position: fixed;
|
| 156 |
+
top: 0;
|
| 157 |
+
right: 0;
|
| 158 |
+
bottom: 0;
|
| 159 |
+
width: 380px;
|
| 160 |
+
background: var(--ed-surface);
|
| 161 |
+
border-left: 1px solid var(--ed-border);
|
| 162 |
+
z-index: var(--z-modal, 1100);
|
| 163 |
+
transform: translateX(100%);
|
| 164 |
+
transition: transform 0.25s ease;
|
| 165 |
+
overflow-y: auto;
|
| 166 |
+
}
|
| 167 |
+
.drawer-panel.open { transform: translateX(0); }
|
| 168 |
+
|
| 169 |
+
/* ---- Form input / textarea ---- */
|
| 170 |
+
.form-input {
|
| 171 |
+
display: block;
|
| 172 |
+
width: 100%;
|
| 173 |
+
padding: 8px 12px;
|
| 174 |
+
background: var(--ed-bg);
|
| 175 |
+
border: 1px solid var(--ed-border);
|
| 176 |
+
border-radius: var(--ed-radius-sm);
|
| 177 |
+
color: var(--ed-text);
|
| 178 |
+
font-size: 0.875rem;
|
| 179 |
+
font-family: inherit;
|
| 180 |
+
outline: none;
|
| 181 |
+
transition: border-color 0.15s;
|
| 182 |
+
}
|
| 183 |
+
.form-input:focus { border-color: var(--primary-color); }
|
| 184 |
+
.form-input::placeholder { color: var(--ed-text-disabled); }
|
| 185 |
+
textarea.form-input { resize: vertical; min-height: 60px; }
|
| 186 |
+
|
| 187 |
+
/* ---- Form select ---- */
|
| 188 |
+
.form-select {
|
| 189 |
+
display: block;
|
| 190 |
+
width: 100%;
|
| 191 |
+
padding: 8px 12px;
|
| 192 |
+
background: var(--ed-bg);
|
| 193 |
+
border: 1px solid var(--ed-border);
|
| 194 |
+
border-radius: var(--ed-radius-sm);
|
| 195 |
+
color: var(--ed-text);
|
| 196 |
+
font-size: 0.875rem;
|
| 197 |
+
font-family: inherit;
|
| 198 |
+
outline: none;
|
| 199 |
+
cursor: pointer;
|
| 200 |
+
transition: border-color 0.15s;
|
| 201 |
+
}
|
| 202 |
+
.form-select:focus { border-color: var(--primary-color); }
|
| 203 |
+
|
| 204 |
+
/* ---- Switch ---- */
|
| 205 |
+
.form-switch-label {
|
| 206 |
+
display: flex;
|
| 207 |
+
align-items: center;
|
| 208 |
+
gap: 8px;
|
| 209 |
+
cursor: pointer;
|
| 210 |
+
font-size: 0.85rem;
|
| 211 |
+
color: var(--ed-text);
|
| 212 |
+
}
|
| 213 |
+
.form-switch {
|
| 214 |
+
position: relative;
|
| 215 |
+
display: inline-block;
|
| 216 |
+
width: 34px;
|
| 217 |
+
height: 20px;
|
| 218 |
+
flex-shrink: 0;
|
| 219 |
+
}
|
| 220 |
+
.form-switch input { opacity: 0; width: 0; height: 0; position: absolute; }
|
| 221 |
+
.form-switch__track {
|
| 222 |
+
position: absolute;
|
| 223 |
+
inset: 0;
|
| 224 |
+
background: var(--ed-border);
|
| 225 |
+
border-radius: 10px;
|
| 226 |
+
transition: background 0.2s;
|
| 227 |
+
}
|
| 228 |
+
.form-switch input:checked + .form-switch__track { background: var(--primary-color); }
|
| 229 |
+
.form-switch__thumb {
|
| 230 |
+
position: absolute;
|
| 231 |
+
top: 2px;
|
| 232 |
+
left: 2px;
|
| 233 |
+
width: 16px;
|
| 234 |
+
height: 16px;
|
| 235 |
+
background: white;
|
| 236 |
+
border-radius: 50%;
|
| 237 |
+
transition: transform 0.2s;
|
| 238 |
+
pointer-events: none;
|
| 239 |
+
}
|
| 240 |
+
.form-switch input:checked ~ .form-switch__thumb { transform: translateX(14px); }
|
| 241 |
+
.form-switch input:focus-visible + .form-switch__track {
|
| 242 |
+
outline: 2px solid var(--primary-color);
|
| 243 |
+
outline-offset: 2px;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
/* ---- Field group label ---- */
|
| 247 |
+
.field-label {
|
| 248 |
+
display: block;
|
| 249 |
+
font-size: 0.75rem;
|
| 250 |
+
font-weight: 500;
|
| 251 |
+
color: var(--ed-text-secondary);
|
| 252 |
+
margin-bottom: 4px;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
/* ---- Tooltip ---- */
|
| 256 |
+
.ed-tooltip {
|
| 257 |
+
position: fixed;
|
| 258 |
+
z-index: var(--z-tooltip, 1200);
|
| 259 |
+
pointer-events: none;
|
| 260 |
+
background: #333;
|
| 261 |
+
color: #fff;
|
| 262 |
+
font-size: 0.7rem;
|
| 263 |
+
padding: 4px 8px;
|
| 264 |
+
border-radius: 4px;
|
| 265 |
+
white-space: nowrap;
|
| 266 |
+
line-height: 1.4;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
/* ---- Bubble toolbar ---- */
|
| 270 |
+
.bubble-toolbar {
|
| 271 |
+
display: flex;
|
| 272 |
+
align-items: center;
|
| 273 |
+
gap: 2px;
|
| 274 |
+
background: #1a1a1a;
|
| 275 |
+
border: 1px solid #333;
|
| 276 |
+
border-radius: 8px;
|
| 277 |
+
padding: 2px 4px;
|
| 278 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
| 279 |
+
}
|
| 280 |
+
.bubble-toolbar .icon-btn { color: rgba(255, 255, 255, 0.6); padding: 6px; }
|
| 281 |
+
.bubble-toolbar .icon-btn:hover { color: #fff; background: none; }
|
| 282 |
+
.bubble-toolbar .icon-btn--active { color: #fff; }
|
| 283 |
+
.bubble-toolbar .divider-v { height: 16px; background: #444; margin: 0 2px; }
|
| 284 |
+
|
| 285 |
+
/* ---- Floating actions ---- */
|
| 286 |
+
.floating-actions { display: flex; align-items: flex-start; gap: 4px; }
|
| 287 |
+
.floating-actions__toggle {
|
| 288 |
+
display: inline-flex;
|
| 289 |
+
align-items: center;
|
| 290 |
+
justify-content: center;
|
| 291 |
+
border: 1px solid transparent;
|
| 292 |
+
background: none;
|
| 293 |
+
cursor: pointer;
|
| 294 |
+
padding: 4px;
|
| 295 |
+
border-radius: var(--ed-radius-sm);
|
| 296 |
+
color: var(--ed-text-disabled);
|
| 297 |
+
transition: all 0.15s;
|
| 298 |
+
line-height: 0;
|
| 299 |
+
}
|
| 300 |
+
.floating-actions__toggle:hover { color: var(--ed-text); border-color: var(--ed-border); }
|
| 301 |
+
.floating-actions__toggle--open {
|
| 302 |
+
color: var(--ed-text);
|
| 303 |
+
border-color: var(--ed-border);
|
| 304 |
+
transform: rotate(45deg);
|
| 305 |
+
}
|
| 306 |
+
.floating-actions__toggle svg { width: 20px; height: 20px; }
|
| 307 |
+
.floating-actions__panel {
|
| 308 |
+
display: flex;
|
| 309 |
+
gap: 2px;
|
| 310 |
+
padding: 2px 4px;
|
| 311 |
+
animation: fadeIn 0.15s ease;
|
| 312 |
+
}
|
| 313 |
+
.floating-actions__panel .icon-btn { color: var(--ed-text-secondary); padding: 6px; }
|
| 314 |
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: translateY(0); } }
|
| 315 |
+
|
| 316 |
+
/* ---- Comment card ---- */
|
| 317 |
+
.comment-card {
|
| 318 |
+
padding: 10px;
|
| 319 |
+
cursor: pointer;
|
| 320 |
+
border-left: 3px solid;
|
| 321 |
+
border-radius: var(--ed-radius);
|
| 322 |
+
transition: all 0.15s;
|
| 323 |
+
}
|
| 324 |
+
.comment-card:hover { border-color: var(--primary-color); }
|
| 325 |
+
.comment-card--active {
|
| 326 |
+
border-color: var(--primary-color);
|
| 327 |
+
background: rgba(149, 141, 241, 0.05);
|
| 328 |
+
}
|
| 329 |
+
.comment-card__header { display: flex; align-items: center; gap: 4px; margin-bottom: 4px; }
|
| 330 |
+
.comment-card__time { font-size: 0.65rem; color: var(--ed-text-disabled); margin-left: auto; }
|
| 331 |
+
.comment-card__text { font-size: 0.8rem; color: var(--ed-text); line-height: 1.5; margin-bottom: 6px; }
|
| 332 |
+
.comment-card__actions { display: flex; gap: 2px; }
|
| 333 |
+
.comment-card__actions .icon-btn { padding: 4px; }
|
| 334 |
+
.comment-card__actions .icon-btn--success { color: var(--ed-success); }
|
| 335 |
+
.comment-card__actions .icon-btn--danger { color: var(--ed-error); }
|
| 336 |
+
|
| 337 |
+
/* ---- Top bar ---- */
|
| 338 |
+
.top-bar {
|
| 339 |
+
flex-shrink: 0;
|
| 340 |
+
display: flex;
|
| 341 |
+
align-items: center;
|
| 342 |
+
justify-content: flex-end;
|
| 343 |
+
padding: 6px 16px;
|
| 344 |
+
gap: 4px;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
/* ---- Chat panel (floating) ---- */
|
| 348 |
+
.chat-floating {
|
| 349 |
+
position: fixed;
|
| 350 |
+
bottom: 16px;
|
| 351 |
+
left: 16px;
|
| 352 |
+
width: 360px;
|
| 353 |
+
height: 520px;
|
| 354 |
+
max-height: calc(100vh - 80px);
|
| 355 |
+
border-radius: 12px;
|
| 356 |
+
overflow: hidden;
|
| 357 |
+
display: flex;
|
| 358 |
+
flex-direction: column;
|
| 359 |
+
background: var(--ed-surface);
|
| 360 |
+
border: 1px solid var(--ed-border);
|
| 361 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
| 362 |
+
z-index: var(--z-overlay, 1300);
|
| 363 |
+
}
|
| 364 |
+
.chat-floating__header {
|
| 365 |
+
display: flex;
|
| 366 |
+
align-items: center;
|
| 367 |
+
justify-content: space-between;
|
| 368 |
+
padding: 6px 12px;
|
| 369 |
+
border-bottom: 1px solid var(--ed-border);
|
| 370 |
+
flex-shrink: 0;
|
| 371 |
+
}
|
| 372 |
+
.chat-floating__title {
|
| 373 |
+
font-weight: 600;
|
| 374 |
+
color: var(--ed-text-secondary);
|
| 375 |
+
font-size: 0.7rem;
|
| 376 |
+
}
|
| 377 |
+
.chat-floating__body { flex: 1; overflow: hidden; display: flex; }
|
| 378 |
+
|
| 379 |
+
/* ---- Chat FAB ---- */
|
| 380 |
+
.chat-fab {
|
| 381 |
+
position: fixed;
|
| 382 |
+
bottom: 20px;
|
| 383 |
+
left: 20px;
|
| 384 |
+
width: 48px;
|
| 385 |
+
height: 48px;
|
| 386 |
+
border-radius: 50%;
|
| 387 |
+
border: none;
|
| 388 |
+
background: var(--primary-color);
|
| 389 |
+
color: #000;
|
| 390 |
+
display: flex;
|
| 391 |
+
align-items: center;
|
| 392 |
+
justify-content: center;
|
| 393 |
+
cursor: pointer;
|
| 394 |
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
| 395 |
+
z-index: var(--z-overlay, 1300);
|
| 396 |
+
transition: background 0.15s;
|
| 397 |
+
line-height: 0;
|
| 398 |
+
}
|
| 399 |
+
.chat-fab:hover { background: var(--primary-color-hover); }
|
| 400 |
+
.chat-fab svg { width: 22px; height: 22px; }
|
| 401 |
+
|
| 402 |
+
/* ---- Author form ---- */
|
| 403 |
+
.author-form {
|
| 404 |
+
margin-top: 16px;
|
| 405 |
+
padding: 16px;
|
| 406 |
+
border: 1px solid var(--ed-border);
|
| 407 |
+
border-radius: var(--ed-radius);
|
| 408 |
+
background: var(--ed-surface);
|
| 409 |
+
display: flex;
|
| 410 |
+
flex-direction: column;
|
| 411 |
+
gap: 12px;
|
| 412 |
+
}
|
| 413 |
+
.author-form__title {
|
| 414 |
+
font-weight: 600;
|
| 415 |
+
color: var(--ed-text-secondary);
|
| 416 |
+
text-transform: uppercase;
|
| 417 |
+
letter-spacing: 0.05em;
|
| 418 |
+
font-size: 0.7rem;
|
| 419 |
+
}
|
| 420 |
+
.author-form__row { display: flex; gap: 8px; }
|
| 421 |
+
.author-form__chips { display: flex; flex-wrap: wrap; gap: 4px; }
|
| 422 |
+
.author-form__actions { display: flex; gap: 8px; justify-content: flex-end; }
|
| 423 |
+
|
| 424 |
+
/* ---- Settings drawer ---- */
|
| 425 |
+
.settings-drawer { padding: 20px; display: flex; flex-direction: column; height: 100%; }
|
| 426 |
+
.settings-drawer__header {
|
| 427 |
+
display: flex;
|
| 428 |
+
align-items: center;
|
| 429 |
+
justify-content: space-between;
|
| 430 |
+
margin-bottom: 16px;
|
| 431 |
+
}
|
| 432 |
+
.settings-drawer__title {
|
| 433 |
+
font-weight: 600;
|
| 434 |
+
text-transform: uppercase;
|
| 435 |
+
letter-spacing: 0.05em;
|
| 436 |
+
color: var(--ed-text-secondary);
|
| 437 |
+
font-size: 0.75rem;
|
| 438 |
+
}
|
| 439 |
+
.settings-drawer__body { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; }
|
| 440 |
+
|
| 441 |
+
/* ---- Chat panel ---- */
|
| 442 |
+
.chat-panel {
|
| 443 |
+
display: flex;
|
| 444 |
+
flex-direction: column;
|
| 445 |
+
height: 100%;
|
| 446 |
+
width: 100%;
|
| 447 |
+
}
|
| 448 |
+
.chat-panel__header {
|
| 449 |
+
display: flex;
|
| 450 |
+
align-items: center;
|
| 451 |
+
justify-content: space-between;
|
| 452 |
+
padding: 8px 12px;
|
| 453 |
+
border-bottom: 1px solid var(--ed-border);
|
| 454 |
+
flex-shrink: 0;
|
| 455 |
+
}
|
| 456 |
+
.chat-panel__label {
|
| 457 |
+
font-weight: 600;
|
| 458 |
+
color: var(--ed-text-secondary);
|
| 459 |
+
letter-spacing: 0.05em;
|
| 460 |
+
text-transform: uppercase;
|
| 461 |
+
font-size: 0.7rem;
|
| 462 |
+
}
|
| 463 |
+
.chat-panel__messages {
|
| 464 |
+
flex: 1;
|
| 465 |
+
overflow: auto;
|
| 466 |
+
padding: 8px 12px;
|
| 467 |
+
display: flex;
|
| 468 |
+
flex-direction: column;
|
| 469 |
+
gap: 12px;
|
| 470 |
+
}
|
| 471 |
+
.chat-panel__empty {
|
| 472 |
+
display: flex;
|
| 473 |
+
align-items: center;
|
| 474 |
+
justify-content: center;
|
| 475 |
+
flex: 1;
|
| 476 |
+
opacity: 0.4;
|
| 477 |
+
text-align: center;
|
| 478 |
+
font-size: 0.75rem;
|
| 479 |
+
max-width: 200px;
|
| 480 |
+
margin: 0 auto;
|
| 481 |
+
}
|
| 482 |
+
.chat-panel__thinking {
|
| 483 |
+
display: flex;
|
| 484 |
+
align-items: center;
|
| 485 |
+
gap: 8px;
|
| 486 |
+
padding: 0 8px;
|
| 487 |
+
font-size: 0.75rem;
|
| 488 |
+
color: var(--ed-text-disabled);
|
| 489 |
+
}
|
| 490 |
+
.chat-panel__error {
|
| 491 |
+
padding: 4px 8px;
|
| 492 |
+
background: rgba(244, 67, 54, 0.15);
|
| 493 |
+
border-radius: 4px;
|
| 494 |
+
font-size: 0.75rem;
|
| 495 |
+
color: var(--ed-error);
|
| 496 |
+
}
|
| 497 |
+
.chat-panel__actions {
|
| 498 |
+
display: flex;
|
| 499 |
+
flex-wrap: wrap;
|
| 500 |
+
gap: 4px;
|
| 501 |
+
padding: 6px 12px;
|
| 502 |
+
border-top: 1px solid var(--ed-border);
|
| 503 |
+
flex-shrink: 0;
|
| 504 |
+
}
|
| 505 |
+
.chat-panel__input {
|
| 506 |
+
padding: 8px 12px;
|
| 507 |
+
border-top: 1px solid var(--ed-border);
|
| 508 |
+
flex-shrink: 0;
|
| 509 |
+
}
|
| 510 |
+
.chat-panel__input-row {
|
| 511 |
+
display: flex;
|
| 512 |
+
gap: 4px;
|
| 513 |
+
align-items: flex-end;
|
| 514 |
+
}
|
| 515 |
+
.chat-panel__textarea {
|
| 516 |
+
flex: 1;
|
| 517 |
+
background: transparent;
|
| 518 |
+
border: none;
|
| 519 |
+
color: var(--ed-text);
|
| 520 |
+
font-size: 0.85rem;
|
| 521 |
+
font-family: inherit;
|
| 522 |
+
line-height: 1.5;
|
| 523 |
+
padding: 4px 0;
|
| 524 |
+
resize: none;
|
| 525 |
+
outline: none;
|
| 526 |
+
}
|
| 527 |
+
.chat-panel__textarea::placeholder { color: var(--ed-text-disabled); }
|
| 528 |
+
|
| 529 |
+
/* ---- Chat bubbles ---- */
|
| 530 |
+
.chat-bubble { display: flex; flex-direction: column; gap: 4px; }
|
| 531 |
+
.chat-bubble__text {
|
| 532 |
+
max-width: 95%;
|
| 533 |
+
padding: 6px 12px;
|
| 534 |
+
border-radius: 8px;
|
| 535 |
+
font-size: 0.8rem;
|
| 536 |
+
line-height: 1.6;
|
| 537 |
+
color: var(--ed-text-secondary);
|
| 538 |
+
white-space: pre-wrap;
|
| 539 |
+
word-break: break-word;
|
| 540 |
+
}
|
| 541 |
+
.chat-bubble__text--user {
|
| 542 |
+
align-self: flex-end;
|
| 543 |
+
background: rgba(149, 141, 241, 0.15);
|
| 544 |
+
color: var(--ed-text);
|
| 545 |
+
}
|
| 546 |
+
.chat-bubble__tool {
|
| 547 |
+
display: flex;
|
| 548 |
+
align-items: center;
|
| 549 |
+
gap: 4px;
|
| 550 |
+
padding: 0 12px;
|
| 551 |
+
opacity: 0.6;
|
| 552 |
+
font-size: 0.65rem;
|
| 553 |
+
color: var(--ed-text-secondary);
|
| 554 |
+
}
|
|
@@ -1,47 +0,0 @@
|
|
| 1 |
-
import { createTheme } from "@mui/material/styles";
|
| 2 |
-
|
| 3 |
-
/**
|
| 4 |
-
* Build a MUI theme whose primary color matches the article's
|
| 5 |
-
* --primary-color CSS variable (stored in Yjs settings).
|
| 6 |
-
*/
|
| 7 |
-
export function buildTheme(primaryColor: string) {
|
| 8 |
-
return createTheme({
|
| 9 |
-
palette: {
|
| 10 |
-
mode: "dark",
|
| 11 |
-
background: {
|
| 12 |
-
default: "#0f0f0f",
|
| 13 |
-
paper: "#1a1a1a",
|
| 14 |
-
},
|
| 15 |
-
primary: {
|
| 16 |
-
main: primaryColor,
|
| 17 |
-
},
|
| 18 |
-
text: {
|
| 19 |
-
primary: "#e0e0e0",
|
| 20 |
-
secondary: "#888",
|
| 21 |
-
},
|
| 22 |
-
divider: "#2a2a2a",
|
| 23 |
-
},
|
| 24 |
-
typography: {
|
| 25 |
-
fontFamily:
|
| 26 |
-
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
| 27 |
-
},
|
| 28 |
-
shape: {
|
| 29 |
-
borderRadius: 8,
|
| 30 |
-
},
|
| 31 |
-
components: {
|
| 32 |
-
MuiIconButton: {
|
| 33 |
-
styleOverrides: {
|
| 34 |
-
root: {
|
| 35 |
-
borderRadius: 6,
|
| 36 |
-
padding: 6,
|
| 37 |
-
},
|
| 38 |
-
},
|
| 39 |
-
},
|
| 40 |
-
},
|
| 41 |
-
});
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
export const DEFAULT_HUE = 47;
|
| 45 |
-
export const DEFAULT_PRIMARY = "#c87533";
|
| 46 |
-
|
| 47 |
-
export const theme = buildTheme(DEFAULT_PRIMARY);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,8 +1,14 @@
|
|
| 1 |
import { defineConfig } from "vite";
|
| 2 |
import react from "@vitejs/plugin-react";
|
|
|
|
| 3 |
|
| 4 |
export default defineConfig({
|
| 5 |
plugins: [react()],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
server: {
|
| 7 |
port: 5678,
|
| 8 |
proxy: {
|
|
|
|
| 1 |
import { defineConfig } from "vite";
|
| 2 |
import react from "@vitejs/plugin-react";
|
| 3 |
+
import { resolve } from "path";
|
| 4 |
|
| 5 |
export default defineConfig({
|
| 6 |
plugins: [react()],
|
| 7 |
+
resolve: {
|
| 8 |
+
alias: {
|
| 9 |
+
"#shared": resolve(__dirname, "../backend/src/shared"),
|
| 10 |
+
},
|
| 11 |
+
},
|
| 12 |
server: {
|
| 13 |
port: 5678,
|
| 14 |
proxy: {
|