From 2a9ed21823adb913b503db8927e0a3e103453db6 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sun, 18 Jun 2023 10:47:07 -0700 Subject: [PATCH] local and global graph --- index.d.ts | 8 +- package-lock.json | 690 ++++++++++++++++++++ package.json | 2 + quartz.config.ts | 10 +- quartz/cfg.ts | 1 - quartz/components/ArticleTitle.tsx | 7 +- quartz/components/Graph.tsx | 81 +++ quartz/components/Head.tsx | 69 +- quartz/components/Header.tsx | 10 +- quartz/components/PageTitle.tsx | 23 +- quartz/components/TableOfContents.tsx | 4 +- quartz/components/index.ts | 4 +- quartz/components/scripts/graph.inline.ts | 287 ++++++++ quartz/components/scripts/popover.inline.ts | 43 +- quartz/components/scripts/spa.inline.ts | 6 +- quartz/components/styles/darkmode.scss | 6 +- quartz/components/styles/graph.scss | 68 ++ quartz/components/styles/popover.scss | 33 +- quartz/components/styles/toc.scss | 3 +- quartz/path.ts | 6 +- quartz/plugins/emitters/contentIndex.ts | 7 +- quartz/plugins/emitters/contentPage.tsx | 4 +- quartz/styles/base.scss | 26 +- 23 files changed, 1288 insertions(+), 110 deletions(-) create mode 100644 quartz/components/Graph.tsx create mode 100644 quartz/components/scripts/graph.inline.ts create mode 100644 quartz/components/styles/graph.scss diff --git a/index.d.ts b/index.d.ts index 26ca700c..3b3acfc0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -8,10 +8,4 @@ interface CustomEventMap { "nav": CustomEvent<{ url: string }>; } -declare global { - interface Document { - addEventListener(type: K, - listener: (this: Document, ev: CustomEventMap[K]) => void): void; - dispatchEvent(ev: CustomEventMap[K]): void; - } -} +declare const fetchData: Promise diff --git a/package-lock.json b/package-lock.json index eb3a121c..c9f9b3ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "@napi-rs/simple-git": "^0.1.8", "chalk": "^4.1.2", "cli-spinner": "^0.2.10", + "d3": "^7.8.5", + "d3-force-reuse": "^1.0.1", "esbuild-sass-plugin": "^2.9.0", "github-slugger": "^2.0.0", "globby": "^13.1.4", @@ -54,6 +56,7 @@ }, "devDependencies": { "@types/cli-spinner": "^0.2.1", + "@types/d3": "^7.4.0", "@types/hast": "^2.3.4", "@types/node": "^20.1.2", "@types/pretty-time": "^1.1.2", @@ -894,6 +897,259 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.0.tgz", + "integrity": "sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.5.tgz", + "integrity": "sha512-Qk7fpJ6qFp+26VeQ47WY0mkwXaiq8+76RJcncDEfMc2ocRzXLO67bLFRNI4OX1aGBoPzsM5Y2T+/m1pldOgD+A==", + "dev": true + }, + "node_modules/@types/d3-axis": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.2.tgz", + "integrity": "sha512-uGC7DBh0TZrU/LY43Fd8Qr+2ja1FKmH07q2FoZFHo1eYl8aj87GhfVoY1saJVJiq24rp1+wpI6BvQJMKgQm8oA==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.2.tgz", + "integrity": "sha512-2TEm8KzUG3N7z0TrSKPmbxByBx54M+S9lHoP2J55QuLU0VSQ9mE96EJSAOVNEqd1bbynMjeTS9VHmz8/bSw8rA==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.2.tgz", + "integrity": "sha512-abT/iLHD3sGZwqMTX1TYCMEulr+wBd0SzyOQnjYNLp7sngdOHYtNkMRI5v3w5thoN+BWtlHVDx2Osvq6fxhZWw==", + "dev": true + }, + "node_modules/@types/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", + "dev": true + }, + "node_modules/@types/d3-contour": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.2.tgz", + "integrity": "sha512-k6/bGDoAGJZnZWaKzeB+9glgXCYGvh6YlluxzBREiVo8f/X2vpTEdgPy9DN7Z2i42PZOZ4JDhVdlTSTSkLDPlQ==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", + "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==", + "dev": true + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.2.tgz", + "integrity": "sha512-rxN6sHUXEZYCKV05MEh4z4WpPSqIw+aP7n9ZN6WYAAvZoEAghEK1WeVZMZcHRBwyaKflU43PCUAJNjFxCzPDjg==", + "dev": true + }, + "node_modules/@types/d3-drag": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.2.tgz", + "integrity": "sha512-qmODKEDvyKWVHcWWCOVcuVcOwikLVsyc4q4EBJMREsoQnR2Qoc2cZQUyFUPgO9q4S3qdSqJKBsuefv+h0Qy+tw==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-76pBHCMTvPLt44wFOieouXcGXWOF0AJCceUvaFkxSZEu4VDUdv93JfpMa6VGNFs01FHfuP4a5Ou68eRG1KBfTw==", + "dev": true + }, + "node_modules/@types/d3-ease": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz", + "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==", + "dev": true + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.2.tgz", + "integrity": "sha512-gllwYWozWfbep16N9fByNBDTkJW/SyhH6SGRlXloR7WdtAaBui4plTP+gbUgiEot7vGw/ZZop1yDZlgXXSuzjA==", + "dev": true, + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.4.tgz", + "integrity": "sha512-q7xbVLrWcXvSBBEoadowIUJ7sRpS1yvgMWnzHJggFy5cUZBq2HZL5k/pBSm0GdYWS1vs5/EDwMjSKF55PDY4Aw==", + "dev": true + }, + "node_modules/@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", + "dev": true + }, + "node_modules/@types/d3-geo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.3.tgz", + "integrity": "sha512-bK9uZJS3vuDCNeeXQ4z3u0E7OeJZXjUgzFdSOtNtMCJCLvDtWDwfpRVWlyt3y8EvRzI0ccOu9xlMVirawolSCw==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-9hjRTVoZjRFR6xo8igAJyNXQyPX6Aq++Nhb5ebrUF414dv4jr2MitM2fWiOY475wa3Za7TOS2Gh9fmqEhLTt0A==", + "dev": true + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "dev": true, + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", + "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==", + "dev": true + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz", + "integrity": "sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==", + "dev": true + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz", + "integrity": "sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==", + "dev": true + }, + "node_modules/@types/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==", + "dev": true + }, + "node_modules/@types/d3-scale": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.3.tgz", + "integrity": "sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==", + "dev": true, + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==", + "dev": true + }, + "node_modules/@types/d3-selection": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.5.tgz", + "integrity": "sha512-xCB0z3Hi8eFIqyja3vW8iV01+OHGYR2di/+e+AiOcXIOrY82lcvWW8Ke1DYE/EUVMsBl4Db9RppSBS3X1U6J0w==", + "dev": true + }, + "node_modules/@types/d3-shape": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.1.tgz", + "integrity": "sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A==", + "dev": true, + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", + "dev": true + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz", + "integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==", + "dev": true + }, + "node_modules/@types/d3-timer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz", + "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==", + "dev": true + }, + "node_modules/@types/d3-transition": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.3.tgz", + "integrity": "sha512-/S90Od8Id1wgQNvIA8iFv9jRhCiZcGhPd2qX0bKF/PS+y0W5CrXKgIiELd2CvG1mlQrWK/qlYh3VxicqG1ZvgA==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.3.tgz", + "integrity": "sha512-OWk1yYIIWcZ07+igN6BeoG6rqhnJ/pYe+R1qWFM2DtW49zsoSjgb9G5xB0ZXA8hh2jAzey1XuRmMSoXdKw8MDA==", + "dev": true, + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", @@ -902,6 +1158,12 @@ "@types/ms": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.10", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", + "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==", + "dev": true + }, "node_modules/@types/hast": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", @@ -1290,6 +1552,408 @@ "node": ">= 8" } }, + "node_modules/d3": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force-reuse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d3-force-reuse/-/d3-force-reuse-1.0.1.tgz", + "integrity": "sha512-TyJfszB6JZmzOYr3oDayjm0LE1Fz0wsn9DkDcYopDOXY/M07rTTDGQ5wYQMZjmcobND3+Og53CATORFbFuQUqw==", + "dependencies": { + "d3-quadtree": "^1.0.3" + } + }, + "node_modules/d3-force-reuse/node_modules/d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1318,6 +1982,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dependencies": { + "robust-predicates": "^3.0.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1965,6 +2637,14 @@ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/is-absolute-url": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", @@ -3596,6 +4276,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/run-async": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", @@ -3626,6 +4311,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", diff --git a/package.json b/package.json index 3e42cf70..adc3bcc2 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@napi-rs/simple-git": "^0.1.8", "chalk": "^4.1.2", "cli-spinner": "^0.2.10", + "d3": "^7.8.5", "esbuild-sass-plugin": "^2.9.0", "github-slugger": "^2.0.0", "globby": "^13.1.4", @@ -67,6 +68,7 @@ }, "devDependencies": { "@types/cli-spinner": "^0.2.1", + "@types/d3": "^7.4.0", "@types/hast": "^2.3.4", "@types/node": "^20.1.2", "@types/pretty-time": "^1.1.2", diff --git a/quartz.config.ts b/quartz.config.ts index 57956720..41d65a69 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -4,7 +4,6 @@ import * as Plugin from "./quartz/plugins" const config: QuartzConfig = { configuration: { - siteTitle: "🪴 Quartz 4.0", enableSPA: true, ignorePatterns: ["private", "templates"], theme: { @@ -58,7 +57,11 @@ const config: QuartzConfig = { Plugin.AliasRedirects(), Plugin.ContentPage({ head: Component.Head(), - header: [Component.PageTitle(), Component.Spacer(), Component.Darkmode()], + header: [ + Component.PageTitle({ title: "🪴 Quartz 4.0" }), + Component.Spacer(), + Component.Darkmode() + ], beforeBody: [ Component.ArticleTitle(), Component.ReadingTime(), @@ -66,9 +69,10 @@ const config: QuartzConfig = { ], content: Component.Content(), left: [ - Component.TableOfContents(), ], right: [ + Component.Graph(), + Component.TableOfContents(), ], footer: [] }), diff --git a/quartz/cfg.ts b/quartz/cfg.ts index 14064020..e1cf3af1 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -2,7 +2,6 @@ import { PluginTypes } from "./plugins/types" import { Theme } from "./theme" export interface GlobalConfiguration { - siteTitle: string, /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */ enableSPA: boolean, /** Glob patterns to not search */ diff --git a/quartz/components/ArticleTitle.tsx b/quartz/components/ArticleTitle.tsx index 37950c48..c25769e2 100644 --- a/quartz/components/ArticleTitle.tsx +++ b/quartz/components/ArticleTitle.tsx @@ -4,10 +4,15 @@ function ArticleTitle({ fileData }: QuartzComponentProps) { const title = fileData.frontmatter?.title const displayTitle = fileData.slug === "index" ? undefined : title if (displayTitle) { - return

{displayTitle}

+ return

{displayTitle}

} else { return null } } +ArticleTitle.css = ` +.article-title { + margin: 2rem 0 0 0; +} +` export default (() => ArticleTitle) satisfies QuartzComponentConstructor diff --git a/quartz/components/Graph.tsx b/quartz/components/Graph.tsx new file mode 100644 index 00000000..01461882 --- /dev/null +++ b/quartz/components/Graph.tsx @@ -0,0 +1,81 @@ +import { QuartzComponentConstructor } from "./types" +// @ts-ignore +import script from "./scripts/graph.inline" +import style from "./styles/graph.scss" + +export interface D3Config { + drag: boolean, + zoom: boolean, + depth: number, + scale: number, + repelForce: number, + centerForce: number, + linkDistance: number, + fontSize: number, + opacityScale: number +} + +interface GraphOptions { + localGraph: Partial, + globalGraph: Partial | undefined +} + +const defaultOptions: GraphOptions = { + localGraph: { + drag: true, + zoom: true, + depth: 1, + scale: 1.2, + repelForce: 2, + centerForce: 1, + linkDistance: 30, + fontSize: 0.6, + opacityScale: 3 + }, + globalGraph: { + drag: true, + zoom: true, + depth: -1, + scale: 1.2, + repelForce: 1, + centerForce: 1, + linkDistance: 30, + fontSize: 0.5, + opacityScale: 3 + } +} + +export default ((opts?: GraphOptions) => { + function Graph() { + const localGraph = { ...opts?.localGraph, ...defaultOptions.localGraph } + const globalGraph = { ...opts?.globalGraph, ...defaultOptions.globalGraph } + return
+

Interactive Graph

+
+
+ + + +
+
+
+
+
+ } + + Graph.css = style + Graph.afterDOMLoaded = script + + return Graph +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx index cc9dd77d..a0b62b7a 100644 --- a/quartz/components/Head.tsx +++ b/quartz/components/Head.tsx @@ -2,32 +2,47 @@ import { resolveToRoot } from "../path" import { JSResourceToScriptElement } from "../resources" import { QuartzComponentConstructor, QuartzComponentProps } from "./types" -function Head({ fileData, externalResources }: QuartzComponentProps) { - const slug = fileData.slug! - const title = fileData.frontmatter?.title ?? "Untitled" - const description = fileData.description ?? "No description provided" - const { css, js } = externalResources - const baseDir = resolveToRoot(slug) - const iconPath = baseDir + "/static/icon.png" - const ogImagePath = baseDir + "/static/og-image.png" - - return - {title} - - - - - - - - - - - - - {css.map(href => )} - {js.filter(resource => resource.loadTime === "beforeDOMReady").map(res => JSResourceToScriptElement(res, true))} - +interface Options { + prefetchContentIndex: boolean } -export default (() => Head) satisfies QuartzComponentConstructor +const defaultOptions: Options = { + prefetchContentIndex: true +} + +export default ((opts?: Options) => { + function Head({ fileData, externalResources }: QuartzComponentProps) { + const slug = fileData.slug! + const title = fileData.frontmatter?.title ?? "Untitled" + const description = fileData.description ?? "No description provided" + const { css, js } = externalResources + const baseDir = resolveToRoot(slug) + const iconPath = baseDir + "/static/icon.png" + const ogImagePath = baseDir + "/static/og-image.png" + + const prefetchContentIndex = opts?.prefetchContentIndex ?? defaultOptions.prefetchContentIndex + const contentIndexPath = baseDir + "/static/contentIndex.json" + const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())` + + return + {title} + + + + + + + + + + + + + {prefetchContentIndex && } + {css.map(href => )} + {js.filter(resource => resource.loadTime === "beforeDOMReady").map(res => JSResourceToScriptElement(res, true))} + + } + + return Head +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/Header.tsx b/quartz/components/Header.tsx index 197c4e52..06ae88bb 100644 --- a/quartz/components/Header.tsx +++ b/quartz/components/Header.tsx @@ -1,9 +1,9 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types" function Header({ children }: QuartzComponentProps) { - return
+ return (children.length > 0) ?
{children} -
+
: null } Header.css = ` @@ -11,12 +11,10 @@ header { display: flex; flex-direction: row; align-items: center; - margin: 1em 0 2em 0; - & > h1 { - } + margin: 2em 0; } -header > h1 { +header h1 { margin: 0; flex: auto; } diff --git a/quartz/components/PageTitle.tsx b/quartz/components/PageTitle.tsx index fe6ec3d8..e8377ee0 100644 --- a/quartz/components/PageTitle.tsx +++ b/quartz/components/PageTitle.tsx @@ -1,11 +1,22 @@ import { resolveToRoot } from "../path" import { QuartzComponentConstructor, QuartzComponentProps } from "./types" -function PageTitle({ cfg, fileData }: QuartzComponentProps) { - const title = cfg.siteTitle - const slug = fileData.slug! - const baseDir = resolveToRoot(slug) - return

{title}

+interface Options { + title: string } -export default (() => PageTitle) satisfies QuartzComponentConstructor +export default ((opts?: Options) => { + const title = opts?.title ?? "Untitled Quartz" + function PageTitle({ fileData }: QuartzComponentProps) { + const slug = fileData.slug! + const baseDir = resolveToRoot(slug) + return

{title}

+ } + PageTitle.css = ` + .page-title { + margin: 0; + } + ` + + return PageTitle +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/TableOfContents.tsx b/quartz/components/TableOfContents.tsx index 99e73e9c..f3d90bb1 100644 --- a/quartz/components/TableOfContents.tsx +++ b/quartz/components/TableOfContents.tsx @@ -18,7 +18,7 @@ function TableOfContents({ fileData }: QuartzComponentProps) { return null } - return <> + return
- + } TableOfContents.css = modernStyle TableOfContents.afterDOMLoaded = script diff --git a/quartz/components/index.ts b/quartz/components/index.ts index 72350fea..98132388 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -7,6 +7,7 @@ import ReadingTime from "./ReadingTime" import Spacer from "./Spacer" import TableOfContents from "./TableOfContents" import TagList from "./TagList" +import Graph from "./Graph" export { ArticleTitle, @@ -17,5 +18,6 @@ export { ReadingTime, Spacer, TableOfContents, - TagList + TagList, + Graph } diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts new file mode 100644 index 00000000..40665ca4 --- /dev/null +++ b/quartz/components/scripts/graph.inline.ts @@ -0,0 +1,287 @@ +import { ContentDetails } from "../../plugins/emitters/contentIndex" +import * as d3 from 'd3' + +type NodeData = { + id: string, + text: string, + tags: string[] +} & d3.SimulationNodeDatum + +type LinkData = { + source: string, + target: string +} + +function relative(from: string, to: string) { + const pieces = [location.protocol, '//', location.host, location.pathname] + const url = pieces.join('').slice(0, -from.length) + to + return url +} + +function removeAllChildren(node: HTMLElement) { + while (node.firstChild) { + node.removeChild(node.firstChild) + } +} + +async function renderGraph(container: string, slug: string) { + const graph = document.getElementById(container)! + removeAllChildren(graph) + + let { + drag: enableDrag, + zoom: enableZoom, + depth, + scale, + repelForce, + centerForce, + linkDistance, + fontSize, + opacityScale + } = JSON.parse(graph.dataset["cfg"]!) + + const data = await fetchData + + const links: LinkData[] = [] + for (const [src, details] of Object.entries(data)) { + const outgoing = details.links ?? [] + for (const dest of outgoing) { + if (src in data && dest in data) { + links.push({ source: src, target: dest }) + } + } + } + + const neighbourhood = new Set() + + const wl = [slug, "__SENTINEL"] + if (depth >= 0) { + while (depth >= 0 && wl.length > 0) { + // compute neighbours + const cur = wl.shift() + if (cur === "__SENTINEL") { + depth-- + wl.push("__SENTINEL") + } else { + neighbourhood.add(cur) + const outgoing = links.filter(l => l.source === cur) + const incoming = links.filter(l => l.target === cur) + wl.push(...outgoing.map((l) => l.target), ...incoming.map((l) => l.source)) + } + } + } else { + links.flatMap(l => [l.source, l.target]).forEach((id) => neighbourhood.add(id)) + } + + const graphData: { nodes: NodeData[], links: LinkData[] } = { + nodes: Object.keys(data).filter(id => neighbourhood.has(id)).map(url => ({ id: url, text: data[url]?.title ?? url, tags: data[url]?.tags ?? [] })), + links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)) + } + + const simulation: d3.Simulation = d3 + .forceSimulation(graphData.nodes) + .force("charge", d3.forceManyBody().strength(-100 * repelForce)) + .force( + "link", + d3 + .forceLink(graphData.links) + .id((d: any) => d.id) + .distance(linkDistance), + ) + .force("center", d3.forceCenter().strength(centerForce)) + + const height = Math.max(graph.offsetHeight, 250) + const width = graph.offsetWidth + + const svg = d3 + .select('#' + container) + .append("svg") + .attr("width", width) + .attr("height", height) + .attr('viewBox', [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale]) + + // draw links between nodes + const link = svg + .append("g") + .selectAll("line") + .data(graphData.links) + .join("line") + .attr("class", "link") + .attr("stroke", "var(--lightgray)") + .attr("stroke-width", 2) + + // svg groups + const graphNode = svg.append("g").selectAll("g").data(graphData.nodes).enter().append("g") + + // calculate radius + const color = (d: NodeData) => { + // TODO: does this handle the index page + const isCurrent = d.id === slug + return isCurrent ? "var(--secondary)" : "var(--gray)" + } + + const drag = (simulation: d3.Simulation) => { + function dragstarted(event: any, d: NodeData) { + if (!event.active) simulation.alphaTarget(1).restart() + d.fx = d.x + d.fy = d.y + } + + function dragged(event: any, d: NodeData) { + d.fx = event.x + d.fy = event.y + } + + function dragended(event: any, d: NodeData) { + if (!event.active) simulation.alphaTarget(0) + d.fx = null + d.fy = null + } + + const noop = () => { } + return d3 + .drag() + .on("start", enableDrag ? dragstarted : noop) + .on("drag", enableDrag ? dragged : noop) + .on("end", enableDrag ? dragended : noop) + } + + function nodeRadius(d: NodeData) { + const numLinks = links.filter((l: any) => l.source.id === d.id || l.target.id === d.id).length + return 2 + Math.sqrt(numLinks) + } + + // draw individual nodes + const node = graphNode + .append("circle") + .attr("class", "node") + .attr("id", (d) => d.id) + .attr("r", nodeRadius) + .attr("fill", color) + .style("cursor", "pointer") + .on("click", (_, d) => { + const targ = relative(slug, d.id) + window.spaNavigate(new URL(targ)) + }) + .on("mouseover", function(_, d) { + const neighbours: string[] = data[slug].links ?? [] + const neighbourNodes = d3.selectAll(".node").filter((d) => neighbours.includes(d.id)) + const currentId = d.id + const linkNodes = d3 + .selectAll(".link") + .filter((d: any) => d.source.id === currentId || d.target.id === currentId) + + // highlight neighbour nodes + neighbourNodes.transition().duration(200).attr("fill", color) + + // highlight links + linkNodes.transition().duration(200).attr("stroke", "var(--gray)") + + const bigFont = fontSize * 1.5 + + // show text for self + const parent = this.parentNode as HTMLElement + d3.select(parent) + .raise() + .select("text") + .transition() + .duration(200) + .attr('opacityOld', d3.select(parent).select('text').style("opacity")) + .style('opacity', 1) + .style('font-size', bigFont + 'em') + }) + .on("mouseleave", function(_, d) { + const currentId = d.id + const linkNodes = d3 + .selectAll(".link") + .filter((d: any) => d.source.id === currentId || d.target.id === currentId) + + linkNodes.transition().duration(200).attr("stroke", "var(--lightgray)") + + const parent = this.parentNode as HTMLElement + d3.select(parent) + .select("text") + .transition() + .duration(200) + .style('opacity', d3.select(parent).select('text').attr("opacityOld")) + .style('font-size', fontSize + 'em') + }) + // @ts-ignore + .call(drag(simulation)) + + // draw labels + const labels = graphNode + .append("text") + .attr("dx", 0) + .attr("dy", (d) => nodeRadius(d) + 8 + "px") + .attr("text-anchor", "middle") + .text((d) => data[d.id]?.title || (d.id.charAt(1).toUpperCase() + d.id.slice(2)).replace("-", " ")) + .style('opacity', (opacityScale - 1) / 3.75) + .style("pointer-events", "none") + .style('font-size', fontSize + 'em') + .raise() + // @ts-ignore + .call(drag(simulation)) + + // set panning + if (enableZoom) { + svg.call( + d3 + .zoom() + .extent([ + [0, 0], + [width, height], + ]) + .scaleExtent([0.25, 4]) + .on("zoom", ({ transform }) => { + link.attr("transform", transform) + node.attr("transform", transform) + const scale = transform.k * opacityScale; + const scaledOpacity = Math.max((scale - 1) / 3.75, 0) + labels.attr("transform", transform).style("opacity", scaledOpacity) + }), + ) + } + + // progress the simulation + simulation.on("tick", () => { + link + .attr("x1", (d: any) => d.source.x) + .attr("y1", (d: any) => d.source.y) + .attr("x2", (d: any) => d.target.x) + .attr("y2", (d: any) => d.target.y) + node + .attr("cx", (d: any) => d.x) + .attr("cy", (d: any) => d.y) + labels + .attr("x", (d: any) => d.x) + .attr("y", (d: any) => d.y) + }) +} + +function renderGlobalGraph() { + const slug = document.body.dataset["slug"]! + renderGraph("global-graph-container", slug) + const container = document.getElementById("global-graph-outer") + container?.classList.add("active") + + function hideGlobalGraph(this: HTMLElement, e: HTMLElementEventMap["click"]) { + if (e.target !== this) return + + container?.classList.remove("active") + const graph = document.getElementById("global-graph-container")! + removeAllChildren(graph) + } + + container?.removeEventListener("click", hideGlobalGraph) + container?.addEventListener("click", hideGlobalGraph) +} + +document.addEventListener("nav", async (e: unknown) => { + const slug = (e as CustomEventMap["nav"]).detail.url + await renderGraph("graph-container", slug) + + const containerIcon = document.getElementById("global-graph-icon") + containerIcon?.removeEventListener("click", renderGlobalGraph) + containerIcon?.addEventListener("click", renderGlobalGraph) +}) diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts index 24c6aecb..5cbe170a 100644 --- a/quartz/components/scripts/popover.inline.ts +++ b/quartz/components/scripts/popover.inline.ts @@ -1,39 +1,50 @@ -import { computePosition, inline, shift, autoPlacement } from "@floating-ui/dom" +import { computePosition, flip, inline, shift } from "@floating-ui/dom" document.addEventListener("nav", () => { const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[] const p = new DOMParser() for (const link of links) { link.addEventListener("mouseenter", async ({ clientX, clientY }) => { - if (link.dataset.fetchedPopover === "true") return + async function setPosition(popoverElement: HTMLElement) { + const { x, y } = await computePosition(link, popoverElement, { + middleware: [inline({ + x: clientX, + y: clientY + }), shift(), flip()] + }) + Object.assign(popoverElement.style, { + left: `${x}px`, + top: `${y}px`, + }) + } + + if (link.dataset.fetchedPopover === "true") { + return setPosition(link.lastChild as HTMLElement) + } + const url = link.href + const anchor = new URL(url).hash + if (anchor.startsWith("#")) return + const contents = await fetch(`${url}`) .then((res) => res.text()) .catch((err) => { console.error(err) }) + if (!contents) return const html = p.parseFromString(contents, "text/html") const elts = [...html.getElementsByClassName("popover-hint")] if (elts.length === 0) return - const popoverElement = document.createElement("div") popoverElement.classList.add("popover") - elts.forEach(elt => popoverElement.appendChild(elt)) - - const { x, y } = await computePosition(link, popoverElement, { - middleware: [inline({ - x: clientX, - y: clientY - }), shift(), autoPlacement()] - }) - - Object.assign(popoverElement.style, { - left: `${x}px`, - top: `${y}px`, - }) + const popoverInner = document.createElement("div") + popoverInner.classList.add("popover-inner") + popoverElement.appendChild(popoverInner) + elts.forEach(elt => popoverInner.appendChild(elt)) + setPosition(popoverElement) link.appendChild(popoverElement) link.dataset.fetchedPopover = "true" }) diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts index a129dc41..057aa3da 100644 --- a/quartz/components/scripts/spa.inline.ts +++ b/quartz/components/scripts/spa.inline.ts @@ -29,8 +29,8 @@ const getOpts = ({ target }: Event): { url: URL, scroll?: boolean } | undefined return { url: new URL(href), scroll: 'routerNoscroll' in a.dataset ? false : undefined } } -function notifyNav(slug: string) { - const event = new CustomEvent("nav", { detail: { slug } }) +function notifyNav(url: string) { + const event: CustomEventMap["nav"] = new CustomEvent("nav", { detail: { url } }) document.dispatchEvent(event) } @@ -73,6 +73,8 @@ async function navigate(url: URL, isBack: boolean = false) { delete announcer.dataset.persist } +window.spaNavigate = navigate + function createRouter() { if (typeof window !== "undefined") { window.addEventListener("click", async (event) => { diff --git a/quartz/components/styles/darkmode.scss b/quartz/components/styles/darkmode.scss index 46291d80..730fcd28 100644 --- a/quartz/components/styles/darkmode.scss +++ b/quartz/components/styles/darkmode.scss @@ -1,8 +1,7 @@ .darkmode { - float: right; - padding: 1rem; - min-width: 30px; position: relative; + width: 20px; + height: 20px; & > .toggle { display: none; @@ -16,7 +15,6 @@ width: 20px; height: 20px; top: calc(50% - 10px); - margin: 0 7px; fill: var(--darkgray); transition: opacity 0.1s ease; } diff --git a/quartz/components/styles/graph.scss b/quartz/components/styles/graph.scss new file mode 100644 index 00000000..76323bb2 --- /dev/null +++ b/quartz/components/styles/graph.scss @@ -0,0 +1,68 @@ +.graph { + & > h3 { + font-size: 1rem; + margin: 0 + } + + & > .graph-outer { + border-radius: 5px; + border: 1px solid var(--lightgray); + box-sizing: border-box; + height: 250px; + width: 300px; + margin: 0.5em 0; + position: relative; + + & > #global-graph-icon { + color: var(--dark); + opacity: 0.5; + width: 18px; + height: 18px; + position: absolute; + padding: 0.2rem; + margin: 0.3rem; + top: 0; + right: 0; + border-radius: 4px; + background-color: transparent; + transition: background-color 0.5s ease; + cursor: pointer; + &:hover { + background-color: var(--lightgray); + } + } + + & > #graph-container > svg { + margin-bottom: -5px; + } + } + + & > #global-graph-outer { + position: fixed; + z-index: 9999; + left: 0; + top: 0; + width: 100vw; + height: 100%; + overflow: scroll; + backdrop-filter: blur(4px); + display: none; + + &.active { + display: inline-block; + } + + & > #global-graph-container { + border: 1px solid var(--lightgray); + background-color: var(--light); + border-radius: 5px; + box-sizing: border-box; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + height: 60vh; + width: 50vw; + } + } +} diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss index 0d26d7d3..0006292c 100644 --- a/quartz/components/styles/popover.scss +++ b/quartz/components/styles/popover.scss @@ -3,7 +3,7 @@ opacity: 0; visibility: hidden; } - 50% { + 1% { opacity: 0; } 100% { @@ -15,21 +15,24 @@ .popover { z-index: 999; position: absolute; - overflow: scroll; - width: 30rem; - height: 20rem; - padding: 0 1rem; - margin-top: -1rem; - border: 1px solid var(--lightgray); - background-color: var(--light); - border-radius: 5px; - box-shadow: 6px 6px 36px 0 rgba(0,0,0,0.25); + overflow: visible; + padding: 1rem; - font-weight: initial; + & > .popover-inner { + width: 30rem; + height: 20rem; + padding: 0 1rem 1rem 1rem; + font-weight: initial; + border: 1px solid var(--gray); + background-color: var(--light); + border-radius: 5px; + box-shadow: 6px 6px 36px 0 rgba(0,0,0,0.25); + overflow: scroll; + } visibility: hidden; opacity: 0; - transition: opacity 0.2s ease, visibility 0.2s ease; + transition: opacity 0.3s ease, visibility 0.3s ease; @media all and (max-width: 600px) { display: none !important; @@ -37,7 +40,7 @@ } a:hover .popover, .popover:hover { - animation: dropin 0.5s ease; - opacity: 1; - visibility: visible; + animation: dropin 0.3s ease; + animation-fill-mode: forwards; + animation-delay: 0.2s; } diff --git a/quartz/components/styles/toc.scss b/quartz/components/styles/toc.scss index 1f1a27b7..117d2938 100644 --- a/quartz/components/styles/toc.scss +++ b/quartz/components/styles/toc.scss @@ -30,6 +30,7 @@ button#toc { overflow: hidden; max-height: none; transition: max-height 0.3s ease; + font-size: 0.9rem; & ul { list-style: none; @@ -38,7 +39,7 @@ button#toc { & > li > a { color: var(--dark); opacity: 0.35; - transition: 0.5s ease opacity; + transition: 0.5s ease opacity, 0.3s ease color; &.in-view { opacity: 0.75; } diff --git a/quartz/path.ts b/quartz/path.ts index 4efd7480..3ae51ccb 100644 --- a/quartz/path.ts +++ b/quartz/path.ts @@ -13,10 +13,6 @@ export function trimPathSuffix(fp: string): string { cleanPath = cleanPath.slice(0, -"index".length) } - if (cleanPath === "") { - cleanPath = "./" - } - return cleanPath + anchor } @@ -36,7 +32,7 @@ export function slugify(s: string): string { export function resolveToRoot(slug: string): string { let fp = trimPathSuffix(slug) - if (fp === "./") { + if (fp === "") { return "." } diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts index 8ee8a9ac..0e03c23a 100644 --- a/quartz/plugins/emitters/contentIndex.ts +++ b/quartz/plugins/emitters/contentIndex.ts @@ -14,19 +14,20 @@ const defaultOptions: Options = { indexExternalLinks: false, } -type ContentIndex = Map +export type ContentDetails = { title: string, links?: string[], tags?: string[], content: string, -}> +} export const ContentIndex: QuartzEmitterPlugin = (userOpts) => { const opts = { ...userOpts, ...defaultOptions } return { name: "ContentIndex", async emit(_contentDir, _cfg, content, _resources, emit) { - const fp = "contentIndex" + const fp = path.join("static", "contentIndex") const linkIndex: ContentIndex = new Map() for (const [tree, file] of content) { let slug = trimPathSuffix(file.data.slug!) diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx index 4728920e..d88cf9c3 100644 --- a/quartz/plugins/emitters/contentPage.tsx +++ b/quartz/plugins/emitters/contentPage.tsx @@ -2,7 +2,7 @@ import { JSResourceToScriptElement, StaticResources } from "../../resources" import { QuartzEmitterPlugin } from "../types" import { render } from "preact-render-to-string" import { QuartzComponent } from "../../components/types" -import { resolveToRoot } from "../../path" +import { resolveToRoot, trimPathSuffix } from "../../path" import HeaderConstructor from "../../components/Header" import { QuartzComponentProps } from "../../components/types" import BodyConstructor from "../../components/Body" @@ -56,7 +56,7 @@ export const ContentPage: QuartzEmitterPlugin = (opts) => { const Content = opts.content const doc = - +
{header.map(HeaderComponent => )} diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index 9fc76f94..fcd4a5d1 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -10,8 +10,6 @@ html { body { margin: 0; - height: 100vh; - width: 100vw; max-width: 100%; box-sizing: border-box; background-color: var(--light); @@ -48,31 +46,39 @@ a { } .page { - padding: 4rem 30vw; - margin: 0 auto; + margin: 6rem 35vw 6rem 20vw; max-width: 1000px; position: relative; & .left, & .right { position: fixed; - padding: 0 4rem 0 6rem; - max-width: 30vw; + height: 100vh; + overflow-y: scroll; box-sizing: border-box; - top: 10rem; + display: flex; + flex-direction: column; + top: 0; + gap: 2rem; + padding: 6rem; } & .left { left: 0; + padding-left: 10vw; + width: 20vw; } & .right { right: 0; + padding-right: 10vw; + width: 35vw; } @media all and (max-width: 1200px) { - padding: 25px 5vw; + margin: 25px 5vw; & .left, & .right { padding: 0; + height: initial; max-width: none; position: initial; } @@ -247,3 +253,7 @@ audio, video { width: 100%; border-radius: 5px; } + +.spacer { + flex: 1 1 auto; +}