mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-24 23:15:46 -05:00
Merge branch 'v4' of https://github.com/jackyzha0/quartz
This commit is contained in:
commit
9082dead75
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@ -8,4 +8,4 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
|
||||
@ -161,7 +161,8 @@ document.addEventListener("nav", () => {
|
||||
})
|
||||
```
|
||||
|
||||
It is best practice to also unmount any existing event handlers to prevent memory leaks.
|
||||
It is best practice to track any event handlers via `window.addCleanup` to prevent memory leaks.
|
||||
This will get called on page navigation.
|
||||
|
||||
#### Importing Code
|
||||
|
||||
|
||||
@ -24,14 +24,32 @@ See [documentation on supported types and syntax here](https://help.obsidian.md
|
||||
## Customization
|
||||
|
||||
- Disable callouts: simply pass `callouts: false` to the plugin: `Plugin.ObsidianFlavoredMarkdown({ callouts: false })`
|
||||
- Editing icons: `quartz/plugins/transformers/ofm.ts`
|
||||
- Editing icons: `quartz/styles/callouts.scss`
|
||||
|
||||
### Add custom callouts
|
||||
|
||||
By default, custom callouts are handled by applying the `note` style. To make fancy ones, you have to add these lines to `custom.scss`.
|
||||
|
||||
```scss title="quartz/styles/custom.scss"
|
||||
.callout {
|
||||
&[data-callout="custom"] {
|
||||
--color: #customcolor;
|
||||
--border: #custombordercolor;
|
||||
--bg: #custombg;
|
||||
--callout-icon: url("data:image/svg+xml; utf8, <custom formatted svg>"); //SVG icon code
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!warning]
|
||||
> Don't forget to ensure that the SVG is URL encoded before putting it in the CSS. You can use tools like [this one](https://yoksel.github.io/url-encoder/) to help you do that.
|
||||
|
||||
## Showcase
|
||||
|
||||
> [!info]
|
||||
> Default title
|
||||
|
||||
> [!question]+ Can callouts be nested?
|
||||
> [!question]+ Can callouts be _nested_?
|
||||
>
|
||||
> > [!todo]- Yes!, they can. And collapsed!
|
||||
> >
|
||||
|
||||
@ -3,7 +3,7 @@ title: Recent Notes
|
||||
tags: component
|
||||
---
|
||||
|
||||
Quartz can generate a list of recent notes for based on some filtering and sorting criteria. Though this component isn't included in any [[layout]] by default, you can add it by using `Component.RecentNotes`.
|
||||
Quartz can generate a list of recent notes based on some filtering and sorting criteria. Though this component isn't included in any [[layout]] by default, you can add it by using `Component.RecentNotes` in `quartz.layout.ts`.
|
||||
|
||||
## Customization
|
||||
|
||||
|
||||
1
globals.d.ts
vendored
1
globals.d.ts
vendored
@ -8,5 +8,6 @@ export declare global {
|
||||
}
|
||||
interface Window {
|
||||
spaNavigate(url: URL, isBack: boolean = false)
|
||||
addCleanup(fn: (...args: any[]) => void)
|
||||
}
|
||||
}
|
||||
|
||||
164
package-lock.json
generated
164
package-lock.json
generated
@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@jackyzha0/quartz",
|
||||
"version": "4.1.5",
|
||||
"version": "4.2.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@jackyzha0/quartz",
|
||||
"version": "4.1.5",
|
||||
"version": "4.2.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@floating-ui/dom": "^1.5.3",
|
||||
"@floating-ui/dom": "^1.6.1",
|
||||
"@napi-rs/simple-git": "0.1.14",
|
||||
"async-mutex": "^0.4.0",
|
||||
"async-mutex": "^0.4.1",
|
||||
"chalk": "^5.3.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"cli-spinner": "^0.2.10",
|
||||
@ -27,9 +27,9 @@
|
||||
"hast-util-to-string": "^3.0.0",
|
||||
"is-absolute-url": "^4.0.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lightningcss": "^1.22.1",
|
||||
"lightningcss": "^1.23.0",
|
||||
"mdast-util-find-and-replace": "^3.0.1",
|
||||
"mdast-util-to-hast": "^13.0.2",
|
||||
"mdast-util-to-hast": "^13.1.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"micromorph": "^0.4.5",
|
||||
"preact": "^10.19.3",
|
||||
@ -71,15 +71,15 @@
|
||||
"devDependencies": {
|
||||
"@types/cli-spinner": "^0.2.3",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/hast": "^3.0.3",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.1.2",
|
||||
"@types/node": "^20.11.14",
|
||||
"@types/pretty-time": "^1.1.5",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@types/yargs": "^17.0.32",
|
||||
"esbuild": "^0.19.9",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier": "^3.2.4",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
@ -478,26 +478,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.2.tgz",
|
||||
"integrity": "sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz",
|
||||
"integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.1.3"
|
||||
"@floating-ui/utils": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz",
|
||||
"integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==",
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz",
|
||||
"integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.4.2",
|
||||
"@floating-ui/utils": "^0.1.3"
|
||||
"@floating-ui/core": "^1.6.0",
|
||||
"@floating-ui/utils": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz",
|
||||
"integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A=="
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
|
||||
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
@ -1043,9 +1043,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.3.tgz",
|
||||
"integrity": "sha512-2fYGlaDy/qyLlhidX42wAH0KBi2TCjKMH8CHmBXgRlJ3Y+OXTiqsPQ6IWarZKwF1JoUcAJdPogv1d4b0COTpmQ==",
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
||||
"dependencies": {
|
||||
"@types/unist": "*"
|
||||
}
|
||||
@ -1088,10 +1088,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.3.tgz",
|
||||
"integrity": "sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==",
|
||||
"dev": true
|
||||
"version": "20.11.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.14.tgz",
|
||||
"integrity": "sha512-w3yWCcwULefjP9DmDDsgUskrMoOy5Z8MiwKHr1FvqGPtx7CvJzQvxD7eKpxNtklQxLruxSXWddyeRtyud0RcXQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pretty-time": {
|
||||
"version": "1.1.5",
|
||||
@ -1205,9 +1208,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/async-mutex": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz",
|
||||
"integrity": "sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz",
|
||||
"integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
@ -3027,9 +3030,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.22.1.tgz",
|
||||
"integrity": "sha512-Fy45PhibiNXkm0cK5FJCbfO8Y6jUpD/YcHf/BtuI+jvYYqSXKF4muk61jjE8YxCR9y+hDYIWSzHTc+bwhDE6rQ==",
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.23.0.tgz",
|
||||
"integrity": "sha512-SEArWKMHhqn/0QzOtclIwH5pXIYQOUEkF8DgICd/105O+GCgd7jxjNod/QPnBCSWvpRHQBGVz5fQ9uScby03zA==",
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3"
|
||||
},
|
||||
@ -3041,21 +3044,21 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-darwin-arm64": "1.22.1",
|
||||
"lightningcss-darwin-x64": "1.22.1",
|
||||
"lightningcss-freebsd-x64": "1.22.1",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.22.1",
|
||||
"lightningcss-linux-arm64-gnu": "1.22.1",
|
||||
"lightningcss-linux-arm64-musl": "1.22.1",
|
||||
"lightningcss-linux-x64-gnu": "1.22.1",
|
||||
"lightningcss-linux-x64-musl": "1.22.1",
|
||||
"lightningcss-win32-x64-msvc": "1.22.1"
|
||||
"lightningcss-darwin-arm64": "1.23.0",
|
||||
"lightningcss-darwin-x64": "1.23.0",
|
||||
"lightningcss-freebsd-x64": "1.23.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.23.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.23.0",
|
||||
"lightningcss-linux-arm64-musl": "1.23.0",
|
||||
"lightningcss-linux-x64-gnu": "1.23.0",
|
||||
"lightningcss-linux-x64-musl": "1.23.0",
|
||||
"lightningcss-win32-x64-msvc": "1.23.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.22.1.tgz",
|
||||
"integrity": "sha512-ldvElu+R0QimNTjsKpaZkUv3zf+uefzLy/R1R19jtgOfSRM+zjUCUgDhfEDRmVqJtMwYsdhMI2aJtJChPC6Osg==",
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.23.0.tgz",
|
||||
"integrity": "sha512-kl4Pk3Q2lnE6AJ7Qaij47KNEfY2/UXRZBT/zqGA24B8qwkgllr/j7rclKOf1axcslNXvvUdztjo4Xqh39Yq1aA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -3072,9 +3075,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.22.1.tgz",
|
||||
"integrity": "sha512-5p2rnlVTv6Gpw4PlTLq925nTVh+HFh4MpegX8dPDYJae+NFVjQ67gY7O6iHIzQjLipDiYejFF0yHrhjU3XgLBQ==",
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.23.0.tgz",
|
||||
"integrity": "sha512-KeRFCNoYfDdcolcFXvokVw+PXCapd2yHS1Diko1z1BhRz/nQuD5XyZmxjWdhmhN/zj5sH8YvWsp0/lPLVzqKpg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -3091,9 +3094,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.22.1.tgz",
|
||||
"integrity": "sha512-1FaBtcFrZqB2hkFbAxY//Pnp8koThvyB6AhjbdVqKD4/pu13Rl91fKt2N9qyeQPUt3xy7ORUvSO+dPk3J6EjXg==",
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.23.0.tgz",
|
||||
"integrity": "sha512-xhnhf0bWPuZxcqknvMDRFFo2TInrmQRWZGB0f6YoAsZX8Y+epfjHeeOIGCfAmgF0DgZxHwYc8mIR5tQU9/+ROA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -3110,9 +3113,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.22.1.tgz",
|
||||
"integrity": "sha512-6rub98tYGfE5I5j0BP8t/2d4BZyu1S7Iz9vUkm0H26snAFHYxLfj3RbQn0xHHIePSetjLnhcg3QlfwUAkD/FYg==",
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.23.0.tgz",
|
||||
"integrity": "sha512-fBamf/bULvmWft9uuX+bZske236pUZEoUlaHNBjnueaCTJ/xd8eXgb0cEc7S5o0Nn6kxlauMBnqJpF70Bgq3zg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -3129,9 +3132,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.22.1.tgz",
|
||||
"integrity": "sha512-nYO5qGtb/1kkTZu3FeTiM+2B2TAb7m2DkLCTgQIs2bk2o9aEs7I96fwySKcoHWQAiQDGR9sMux9vkV4KQXqPaQ==",
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.23.0.tgz",
|
||||
"integrity": "sha512-RS7sY77yVLOmZD6xW2uEHByYHhQi5JYWmgVumYY85BfNoVI3DupXSlzbw+b45A9NnVKq45+oXkiN6ouMMtTwfg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -3148,9 +3151,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.22.1.tgz",
|
||||
"integrity": "sha512-MCV6RuRpzXbunvzwY644iz8cw4oQxvW7oer9xPkdadYqlEyiJJ6wl7FyJOH7Q6ZYH4yjGAUCvxDBxPbnDu9ZVg==",
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.23.0.tgz",
|
||||
"integrity": "sha512-cU00LGb6GUXCwof6ACgSMKo3q7XYbsyTj0WsKHLi1nw7pV0NCq8nFTn6ZRBYLoKiV8t+jWl0Hv8KkgymmK5L5g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -3167,9 +3170,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.22.1.tgz",
|
||||
"integrity": "sha512-RjNgpdM20VUXgV7us/VmlO3Vn2ZRiDnc3/bUxCVvySZWPiVPprpqW/QDWuzkGa+NCUf6saAM5CLsZLSxncXJwg==",
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.23.0.tgz",
|
||||
"integrity": "sha512-q4jdx5+5NfB0/qMbXbOmuC6oo7caPnFghJbIAV90cXZqgV8Am3miZhC4p+sQVdacqxfd+3nrle4C8icR3p1AYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -3186,9 +3189,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.22.1.tgz",
|
||||
"integrity": "sha512-ZgO4C7Rd6Hv/5MnyY2KxOYmIlzk4rplVolDt3NbkNR8DndnyX0Q5IR4acJWNTBICQ21j3zySzKbcJaiJpk/4YA==",
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.23.0.tgz",
|
||||
"integrity": "sha512-G9Ri3qpmF4qef2CV/80dADHKXRAQeQXpQTLx7AiQrBYQHqBjB75oxqj06FCIe5g4hNCqLPnM9fsO4CyiT1sFSQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -3205,9 +3208,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.22.1.tgz",
|
||||
"integrity": "sha512-4pozV4eyD0MDET41ZLHAeBo+H04Nm2UEYIk5w/ts40231dRFV7E0cjwbnZvSoc1DXFgecAhiC0L16ruv/ZDCpg==",
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.23.0.tgz",
|
||||
"integrity": "sha512-1rcBDJLU+obPPJM6qR5fgBUiCdZwZLafZM5f9kwjFLkb/UBNIzmae39uCSmh71nzPCTXZqHbvwu23OWnWEz+eg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -3607,9 +3610,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-hast": {
|
||||
"version": "13.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.0.2.tgz",
|
||||
"integrity": "sha512-U5I+500EOOw9e3ZrclN3Is3fRpw8c19SMyNZlZ2IS+7vLsNzb2Om11VpIVOR+/0137GhZsFEF6YiKD5+0Hr2Og==",
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz",
|
||||
"integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
@ -3618,7 +3621,8 @@
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"trim-lines": "^3.0.0",
|
||||
"unist-util-position": "^5.0.0",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@ -4470,9 +4474,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz",
|
||||
"integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==",
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz",
|
||||
"integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
@ -5674,6 +5678,12 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/unherit": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unherit/-/unherit-3.0.1.tgz",
|
||||
|
||||
16
package.json
16
package.json
@ -2,7 +2,7 @@
|
||||
"name": "@jackyzha0/quartz",
|
||||
"description": "🌱 publish your digital garden and notes as a website",
|
||||
"private": true,
|
||||
"version": "4.1.5",
|
||||
"version": "4.2.1",
|
||||
"type": "module",
|
||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||
"license": "MIT",
|
||||
@ -39,9 +39,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@floating-ui/dom": "^1.5.3",
|
||||
"@floating-ui/dom": "^1.6.1",
|
||||
"@napi-rs/simple-git": "0.1.14",
|
||||
"async-mutex": "^0.4.0",
|
||||
"async-mutex": "^0.4.1",
|
||||
"chalk": "^5.3.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"cli-spinner": "^0.2.10",
|
||||
@ -56,9 +56,9 @@
|
||||
"hast-util-to-string": "^3.0.0",
|
||||
"is-absolute-url": "^4.0.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lightningcss": "^1.22.1",
|
||||
"lightningcss": "^1.23.0",
|
||||
"mdast-util-find-and-replace": "^3.0.1",
|
||||
"mdast-util-to-hast": "^13.0.2",
|
||||
"mdast-util-to-hast": "^13.1.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"micromorph": "^0.4.5",
|
||||
"preact": "^10.19.3",
|
||||
@ -97,15 +97,15 @@
|
||||
"devDependencies": {
|
||||
"@types/cli-spinner": "^0.2.3",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/hast": "^3.0.3",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.1.2",
|
||||
"@types/node": "^20.11.14",
|
||||
"@types/pretty-time": "^1.1.5",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@types/yargs": "^17.0.32",
|
||||
"esbuild": "^0.19.9",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier": "^3.2.4",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
|
||||
@ -126,17 +126,8 @@ async function rebuildFromEntrypoint(
|
||||
clientRefresh: () => void,
|
||||
buildData: BuildData, // note: this function mutates buildData
|
||||
) {
|
||||
const {
|
||||
ctx,
|
||||
ignored,
|
||||
mut,
|
||||
initialSlugs,
|
||||
contentMap,
|
||||
toRebuild,
|
||||
toRemove,
|
||||
trackedAssets,
|
||||
lastBuildMs,
|
||||
} = buildData
|
||||
const { ctx, ignored, mut, initialSlugs, contentMap, toRebuild, toRemove, trackedAssets } =
|
||||
buildData
|
||||
|
||||
const { argv } = ctx
|
||||
|
||||
@ -164,12 +155,12 @@ async function rebuildFromEntrypoint(
|
||||
toRemove.add(filePath)
|
||||
}
|
||||
|
||||
// debounce rebuilds every 250ms
|
||||
|
||||
const buildStart = new Date().getTime()
|
||||
buildData.lastBuildMs = buildStart
|
||||
const release = await mut.acquire()
|
||||
if (lastBuildMs > buildStart) {
|
||||
|
||||
// there's another build after us, release and let them do it
|
||||
if (buildData.lastBuildMs > buildStart) {
|
||||
release()
|
||||
return
|
||||
}
|
||||
|
||||
@ -4,8 +4,18 @@ import style from "./styles/search.scss"
|
||||
import script from "./scripts/search.inline"
|
||||
import { classNames } from "../util/lang"
|
||||
|
||||
export default (() => {
|
||||
export interface SearchOptions {
|
||||
enablePreview: boolean
|
||||
}
|
||||
|
||||
const defaultOptions: SearchOptions = {
|
||||
enablePreview: true,
|
||||
}
|
||||
|
||||
export default ((userOpts?: Partial<SearchOptions>) => {
|
||||
function Search({ displayClass }: QuartzComponentProps) {
|
||||
const opts = { ...defaultOptions, ...userOpts }
|
||||
|
||||
return (
|
||||
<div class={classNames(displayClass, "search")}>
|
||||
<div id="search-icon">
|
||||
@ -36,7 +46,7 @@ export default (() => {
|
||||
aria-label="Search for something"
|
||||
placeholder="Search for something"
|
||||
/>
|
||||
<div id="results-container"></div>
|
||||
<div id="search-layout" data-preview={opts.enablePreview}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
function toggleCallout(this: HTMLElement) {
|
||||
const outerBlock = this.parentElement!
|
||||
outerBlock.classList.toggle(`is-collapsed`)
|
||||
const collapsed = outerBlock.classList.contains(`is-collapsed`)
|
||||
outerBlock.classList.toggle("is-collapsed")
|
||||
const collapsed = outerBlock.classList.contains("is-collapsed")
|
||||
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
|
||||
outerBlock.style.maxHeight = height + `px`
|
||||
outerBlock.style.maxHeight = height + "px"
|
||||
|
||||
// walk and adjust height of all parents
|
||||
let current = outerBlock
|
||||
let parent = outerBlock.parentElement
|
||||
while (parent) {
|
||||
if (!parent.classList.contains(`callout`)) {
|
||||
if (!parent.classList.contains("callout")) {
|
||||
return
|
||||
}
|
||||
|
||||
const collapsed = parent.classList.contains(`is-collapsed`)
|
||||
const collapsed = parent.classList.contains("is-collapsed")
|
||||
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
|
||||
parent.style.maxHeight = height + `px`
|
||||
parent.style.maxHeight = height + "px"
|
||||
|
||||
current = parent
|
||||
parent = parent.parentElement
|
||||
@ -30,15 +30,15 @@ function setupCallout() {
|
||||
const title = div.firstElementChild
|
||||
|
||||
if (title) {
|
||||
title.removeEventListener(`click`, toggleCallout)
|
||||
title.addEventListener(`click`, toggleCallout)
|
||||
title.addEventListener("click", toggleCallout)
|
||||
window.addCleanup(() => title.removeEventListener("click", toggleCallout))
|
||||
|
||||
const collapsed = div.classList.contains(`is-collapsed`)
|
||||
const collapsed = div.classList.contains("is-collapsed")
|
||||
const height = collapsed ? title.scrollHeight : div.scrollHeight
|
||||
div.style.maxHeight = height + `px`
|
||||
div.style.maxHeight = height + "px"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener(`nav`, setupCallout)
|
||||
window.addEventListener(`resize`, setupCallout)
|
||||
document.addEventListener("nav", setupCallout)
|
||||
window.addEventListener("resize", setupCallout)
|
||||
|
||||
@ -14,7 +14,7 @@ document.addEventListener("nav", () => {
|
||||
button.type = "button"
|
||||
button.innerHTML = svgCopy
|
||||
button.ariaLabel = "Copy source"
|
||||
button.addEventListener("click", () => {
|
||||
function onClick() {
|
||||
navigator.clipboard.writeText(source).then(
|
||||
() => {
|
||||
button.blur()
|
||||
@ -26,7 +26,9 @@ document.addEventListener("nav", () => {
|
||||
},
|
||||
(error) => console.error(error),
|
||||
)
|
||||
})
|
||||
}
|
||||
button.addEventListener("click", onClick)
|
||||
window.addCleanup(() => button.removeEventListener("click", onClick))
|
||||
els[i].prepend(button)
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,28 +10,31 @@ const emitThemeChangeEvent = (theme: "light" | "dark") => {
|
||||
}
|
||||
|
||||
document.addEventListener("nav", () => {
|
||||
const switchTheme = (e: any) => {
|
||||
const newTheme = e.target.checked ? "dark" : "light"
|
||||
const switchTheme = (e: Event) => {
|
||||
const newTheme = (e.target as HTMLInputElement)?.checked ? "dark" : "light"
|
||||
document.documentElement.setAttribute("saved-theme", newTheme)
|
||||
localStorage.setItem("theme", newTheme)
|
||||
emitThemeChangeEvent(newTheme)
|
||||
}
|
||||
|
||||
const themeChange = (e: MediaQueryListEvent) => {
|
||||
const newTheme = e.matches ? "dark" : "light"
|
||||
document.documentElement.setAttribute("saved-theme", newTheme)
|
||||
localStorage.setItem("theme", newTheme)
|
||||
toggleSwitch.checked = e.matches
|
||||
emitThemeChangeEvent(newTheme)
|
||||
}
|
||||
|
||||
// Darkmode toggle
|
||||
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
|
||||
toggleSwitch.removeEventListener("change", switchTheme)
|
||||
toggleSwitch.addEventListener("change", switchTheme)
|
||||
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
|
||||
if (currentTheme === "dark") {
|
||||
toggleSwitch.checked = true
|
||||
}
|
||||
|
||||
// Listen for changes in prefers-color-scheme
|
||||
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
colorSchemeMediaQuery.addEventListener("change", (e) => {
|
||||
const newTheme = e.matches ? "dark" : "light"
|
||||
document.documentElement.setAttribute("saved-theme", newTheme)
|
||||
localStorage.setItem("theme", newTheme)
|
||||
toggleSwitch.checked = e.matches
|
||||
emitThemeChangeEvent(newTheme)
|
||||
})
|
||||
colorSchemeMediaQuery.addEventListener("change", themeChange)
|
||||
window.addCleanup(() => colorSchemeMediaQuery.removeEventListener("change", themeChange))
|
||||
})
|
||||
|
||||
@ -57,20 +57,20 @@ function setupExplorer() {
|
||||
for (const item of document.getElementsByClassName(
|
||||
"folder-button",
|
||||
) as HTMLCollectionOf<HTMLElement>) {
|
||||
item.removeEventListener("click", toggleFolder)
|
||||
item.addEventListener("click", toggleFolder)
|
||||
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
|
||||
}
|
||||
}
|
||||
|
||||
explorer.removeEventListener("click", toggleExplorer)
|
||||
explorer.addEventListener("click", toggleExplorer)
|
||||
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
|
||||
|
||||
// Set up click handlers for each folder (click handler on folder "icon")
|
||||
for (const item of document.getElementsByClassName(
|
||||
"folder-icon",
|
||||
) as HTMLCollectionOf<HTMLElement>) {
|
||||
item.removeEventListener("click", toggleFolder)
|
||||
item.addEventListener("click", toggleFolder)
|
||||
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
|
||||
}
|
||||
|
||||
// Get folder state from local storage
|
||||
|
||||
@ -325,6 +325,6 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
await renderGraph("graph-container", slug)
|
||||
|
||||
const containerIcon = document.getElementById("global-graph-icon")
|
||||
containerIcon?.removeEventListener("click", renderGlobalGraph)
|
||||
containerIcon?.addEventListener("click", renderGlobalGraph)
|
||||
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
|
||||
})
|
||||
|
||||
@ -76,7 +76,7 @@ async function mouseEnterHandler(
|
||||
document.addEventListener("nav", () => {
|
||||
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
|
||||
for (const link of links) {
|
||||
link.removeEventListener("mouseenter", mouseEnterHandler)
|
||||
link.addEventListener("mouseenter", mouseEnterHandler)
|
||||
window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler))
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import FlexSearch from "flexsearch"
|
||||
import { ContentDetails } from "../../plugins/emitters/contentIndex"
|
||||
import { registerEscapeHandler, removeAllChildren } from "./util"
|
||||
import { FullSlug, resolveRelative } from "../../util/path"
|
||||
import { FullSlug, normalizeRelativeURLs, resolveRelative } from "../../util/path"
|
||||
|
||||
interface Item {
|
||||
id: number
|
||||
@ -11,23 +11,53 @@ interface Item {
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
let index: FlexSearch.Document<Item> | undefined = undefined
|
||||
|
||||
// Can be expanded with things like "term" in the future
|
||||
type SearchType = "basic" | "tags"
|
||||
|
||||
// Current searchType
|
||||
let searchType: SearchType = "basic"
|
||||
let currentSearchTerm: string = ""
|
||||
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
||||
let index = new FlexSearch.Document<Item>({
|
||||
charset: "latin:extra",
|
||||
encode: encoder,
|
||||
document: {
|
||||
id: "id",
|
||||
index: [
|
||||
{
|
||||
field: "title",
|
||||
tokenize: "forward",
|
||||
},
|
||||
{
|
||||
field: "content",
|
||||
tokenize: "forward",
|
||||
},
|
||||
{
|
||||
field: "tags",
|
||||
tokenize: "forward",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const p = new DOMParser()
|
||||
const fetchContentCache: Map<FullSlug, Element[]> = new Map()
|
||||
const contextWindowWords = 30
|
||||
const numSearchResults = 5
|
||||
const numTagResults = 3
|
||||
const numSearchResults = 8
|
||||
const numTagResults = 5
|
||||
|
||||
const tokenizeTerm = (term: string) => {
|
||||
const tokens = term.split(/\s+/).filter((t) => t.trim() !== "")
|
||||
const tokenLen = tokens.length
|
||||
if (tokenLen > 1) {
|
||||
for (let i = 1; i < tokenLen; i++) {
|
||||
tokens.push(tokens.slice(0, i + 1).join(" "))
|
||||
}
|
||||
}
|
||||
|
||||
return tokens.sort((a, b) => b.length - a.length) // always highlight longest terms first
|
||||
}
|
||||
|
||||
function highlight(searchTerm: string, text: string, trim?: boolean) {
|
||||
// try to highlight longest tokens first
|
||||
const tokenizedTerms = searchTerm
|
||||
.split(/\s+/)
|
||||
.filter((t) => t !== "")
|
||||
.sort((a, b) => b.length - a.length)
|
||||
const tokenizedTerms = tokenizeTerm(searchTerm)
|
||||
let tokenizedText = text.split(/\s+/).filter((t) => t !== "")
|
||||
|
||||
let startIndex = 0
|
||||
@ -71,20 +101,78 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
|
||||
}`
|
||||
}
|
||||
|
||||
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
||||
let prevShortcutHandler: ((e: HTMLElementEventMap["keydown"]) => void) | undefined = undefined
|
||||
document.addEventListener("nav", async (e: unknown) => {
|
||||
const currentSlug = (e as CustomEventMap["nav"]).detail.url
|
||||
function highlightHTML(searchTerm: string, el: HTMLElement) {
|
||||
const p = new DOMParser()
|
||||
const tokenizedTerms = tokenizeTerm(searchTerm)
|
||||
const html = p.parseFromString(el.innerHTML, "text/html")
|
||||
|
||||
const createHighlightSpan = (text: string) => {
|
||||
const span = document.createElement("span")
|
||||
span.className = "highlight"
|
||||
span.textContent = text
|
||||
return span
|
||||
}
|
||||
|
||||
const highlightTextNodes = (node: Node, term: string) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const nodeText = node.nodeValue ?? ""
|
||||
const regex = new RegExp(term.toLowerCase(), "gi")
|
||||
const matches = nodeText.match(regex)
|
||||
if (!matches || matches.length === 0) return
|
||||
const spanContainer = document.createElement("span")
|
||||
let lastIndex = 0
|
||||
for (const match of matches) {
|
||||
const matchIndex = nodeText.indexOf(match, lastIndex)
|
||||
spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex, matchIndex)))
|
||||
spanContainer.appendChild(createHighlightSpan(match))
|
||||
lastIndex = matchIndex + match.length
|
||||
}
|
||||
spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex)))
|
||||
node.parentNode?.replaceChild(spanContainer, node)
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
if ((node as HTMLElement).classList.contains("highlight")) return
|
||||
Array.from(node.childNodes).forEach((child) => highlightTextNodes(child, term))
|
||||
}
|
||||
}
|
||||
|
||||
for (const term of tokenizedTerms) {
|
||||
highlightTextNodes(html.body, term)
|
||||
}
|
||||
|
||||
return html.body
|
||||
}
|
||||
|
||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
const currentSlug = e.detail.url
|
||||
const data = await fetchData
|
||||
const container = document.getElementById("search-container")
|
||||
const sidebar = container?.closest(".sidebar") as HTMLElement
|
||||
const searchIcon = document.getElementById("search-icon")
|
||||
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
|
||||
const results = document.getElementById("results-container")
|
||||
const resultCards = document.getElementsByClassName("result-card")
|
||||
const searchLayout = document.getElementById("search-layout")
|
||||
const idDataMap = Object.keys(data) as FullSlug[]
|
||||
|
||||
const appendLayout = (el: HTMLElement) => {
|
||||
if (searchLayout?.querySelector(`#${el.id}`) === null) {
|
||||
searchLayout?.appendChild(el)
|
||||
}
|
||||
}
|
||||
|
||||
const enablePreview = searchLayout?.dataset?.preview === "true"
|
||||
let preview: HTMLDivElement | undefined = undefined
|
||||
let previewInner: HTMLDivElement | undefined = undefined
|
||||
const results = document.createElement("div")
|
||||
results.id = "results-container"
|
||||
results.style.flexBasis = enablePreview ? "min(30%, 450px)" : "100%"
|
||||
appendLayout(results)
|
||||
|
||||
if (enablePreview) {
|
||||
preview = document.createElement("div")
|
||||
preview.id = "preview-container"
|
||||
preview.style.flexBasis = "100%"
|
||||
appendLayout(preview)
|
||||
}
|
||||
|
||||
function hideSearch() {
|
||||
container?.classList.remove("active")
|
||||
if (searchBar) {
|
||||
@ -96,6 +184,12 @@ document.addEventListener("nav", async (e: unknown) => {
|
||||
if (results) {
|
||||
removeAllChildren(results)
|
||||
}
|
||||
if (preview) {
|
||||
removeAllChildren(preview)
|
||||
}
|
||||
if (searchLayout) {
|
||||
searchLayout.classList.remove("display-results")
|
||||
}
|
||||
|
||||
searchType = "basic" // reset search type after closing
|
||||
}
|
||||
@ -109,11 +203,14 @@ document.addEventListener("nav", async (e: unknown) => {
|
||||
searchBar?.focus()
|
||||
}
|
||||
|
||||
function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
||||
let currentHover: HTMLInputElement | null = null
|
||||
|
||||
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
||||
if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
const searchBarOpen = container?.classList.contains("active")
|
||||
searchBarOpen ? hideSearch() : showSearch("basic")
|
||||
return
|
||||
} else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
|
||||
// Hotkey to open tag search
|
||||
e.preventDefault()
|
||||
@ -122,159 +219,227 @@ document.addEventListener("nav", async (e: unknown) => {
|
||||
|
||||
// add "#" prefix for tag search
|
||||
if (searchBar) searchBar.value = "#"
|
||||
return
|
||||
}
|
||||
|
||||
if (currentHover) {
|
||||
currentHover.classList.remove("focus")
|
||||
currentHover.blur()
|
||||
}
|
||||
|
||||
// If search is active, then we will render the first result and display accordingly
|
||||
if (!container?.classList.contains("active")) return
|
||||
else if (e.key === "Enter") {
|
||||
// If result has focus, navigate to that one, otherwise pick first result
|
||||
if (results?.contains(document.activeElement)) {
|
||||
const active = document.activeElement as HTMLInputElement
|
||||
if (active.classList.contains("no-match")) return
|
||||
await displayPreview(active)
|
||||
active.click()
|
||||
} else {
|
||||
const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null
|
||||
anchor?.click()
|
||||
if (!anchor || anchor?.classList.contains("no-match")) return
|
||||
await displayPreview(anchor)
|
||||
anchor.click()
|
||||
}
|
||||
} else if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) {
|
||||
e.preventDefault()
|
||||
if (results?.contains(document.activeElement)) {
|
||||
// If an element in results-container already has focus, focus previous one
|
||||
const prevResult = document.activeElement?.previousElementSibling as HTMLInputElement | null
|
||||
const currentResult = currentHover
|
||||
? currentHover
|
||||
: (document.activeElement as HTMLInputElement | null)
|
||||
const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null
|
||||
currentResult?.classList.remove("focus")
|
||||
prevResult?.focus()
|
||||
currentHover = prevResult
|
||||
await displayPreview(prevResult)
|
||||
}
|
||||
} else if (e.key === "ArrowDown" || e.key === "Tab") {
|
||||
e.preventDefault()
|
||||
// When first pressing ArrowDown, results wont contain the active element, so focus first element
|
||||
if (!results?.contains(document.activeElement)) {
|
||||
const firstResult = resultCards[0] as HTMLInputElement | null
|
||||
firstResult?.focus()
|
||||
// The results should already been focused, so we need to find the next one.
|
||||
// The activeElement is the search bar, so we need to find the first result and focus it.
|
||||
if (document.activeElement === searchBar || currentHover !== null) {
|
||||
const firstResult = currentHover
|
||||
? currentHover
|
||||
: (document.getElementsByClassName("result-card")[0] as HTMLInputElement | null)
|
||||
const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null
|
||||
firstResult?.classList.remove("focus")
|
||||
secondResult?.focus()
|
||||
currentHover = secondResult
|
||||
await displayPreview(secondResult)
|
||||
} else {
|
||||
// If an element in results-container already has focus, focus next one
|
||||
const nextResult = document.activeElement?.nextElementSibling as HTMLInputElement | null
|
||||
const active = currentHover
|
||||
? currentHover
|
||||
: (document.activeElement as HTMLInputElement | null)
|
||||
active?.classList.remove("focus")
|
||||
const nextResult = active?.nextElementSibling as HTMLInputElement | null
|
||||
nextResult?.focus()
|
||||
currentHover = nextResult
|
||||
await displayPreview(nextResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function trimContent(content: string) {
|
||||
// works without escaping html like in `description.ts`
|
||||
const sentences = content.replace(/\s+/g, " ").split(".")
|
||||
let finalDesc = ""
|
||||
let sentenceIdx = 0
|
||||
|
||||
// Roughly estimate characters by (words * 5). Matches description length in `description.ts`.
|
||||
const len = contextWindowWords * 5
|
||||
while (finalDesc.length < len) {
|
||||
const sentence = sentences[sentenceIdx]
|
||||
if (!sentence) break
|
||||
finalDesc += sentence + "."
|
||||
sentenceIdx++
|
||||
}
|
||||
|
||||
// If more content would be available, indicate it by finishing with "..."
|
||||
if (finalDesc.length < content.length) {
|
||||
finalDesc += ".."
|
||||
}
|
||||
|
||||
return finalDesc
|
||||
}
|
||||
|
||||
const formatForDisplay = (term: string, id: number) => {
|
||||
const slug = idDataMap[id]
|
||||
return {
|
||||
id,
|
||||
slug,
|
||||
title: searchType === "tags" ? data[slug].title : highlight(term, data[slug].title ?? ""),
|
||||
// if searchType is tag, display context from start of file and trim, otherwise use regular highlight
|
||||
content:
|
||||
searchType === "tags"
|
||||
? trimContent(data[slug].content)
|
||||
: highlight(term, data[slug].content ?? "", true),
|
||||
tags: highlightTags(term, data[slug].tags),
|
||||
content: highlight(term, data[slug].content ?? "", true),
|
||||
tags: highlightTags(term.substring(1), data[slug].tags),
|
||||
}
|
||||
}
|
||||
|
||||
function highlightTags(term: string, tags: string[]) {
|
||||
if (tags && searchType === "tags") {
|
||||
// Find matching tags
|
||||
const termLower = term.toLowerCase()
|
||||
let matching = tags.filter((str) => str.includes(termLower))
|
||||
|
||||
// Subtract matching from original tags, then push difference
|
||||
if (matching.length > 0) {
|
||||
let difference = tags.filter((x) => !matching.includes(x))
|
||||
|
||||
// Convert to html (cant be done later as matches/term dont get passed to `resultToHTML`)
|
||||
matching = matching.map((tag) => `<li><p class="match-tag">#${tag}</p></li>`)
|
||||
difference = difference.map((tag) => `<li><p>#${tag}</p></li>`)
|
||||
matching.push(...difference)
|
||||
}
|
||||
|
||||
// Only allow max of `numTagResults` in preview
|
||||
if (tags.length > numTagResults) {
|
||||
matching.splice(numTagResults)
|
||||
}
|
||||
|
||||
return matching
|
||||
} else {
|
||||
if (!tags || searchType !== "tags") {
|
||||
return []
|
||||
}
|
||||
|
||||
return tags
|
||||
.map((tag) => {
|
||||
if (tag.toLowerCase().includes(term.toLowerCase())) {
|
||||
return `<li><p class="match-tag">#${tag}</p></li>`
|
||||
} else {
|
||||
return `<li><p>#${tag}</p></li>`
|
||||
}
|
||||
})
|
||||
.slice(0, numTagResults)
|
||||
}
|
||||
|
||||
function resolveUrl(slug: FullSlug): URL {
|
||||
return new URL(resolveRelative(currentSlug, slug), location.toString())
|
||||
}
|
||||
|
||||
const resultToHTML = ({ slug, title, content, tags }: Item) => {
|
||||
const htmlTags = tags.length > 0 ? `<ul>${tags.join("")}</ul>` : ``
|
||||
const htmlTags = tags.length > 0 ? `<ul class="tags">${tags.join("")}</ul>` : ``
|
||||
const itemTile = document.createElement("a")
|
||||
itemTile.classList.add("result-card")
|
||||
itemTile.id = slug
|
||||
itemTile.href = new URL(resolveRelative(currentSlug, slug), location.toString()).toString()
|
||||
itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}<p>${content}</p>`
|
||||
itemTile.addEventListener("click", (event) => {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
|
||||
hideSearch()
|
||||
itemTile.href = resolveUrl(slug).toString()
|
||||
itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}<p class="preview">${content}</p>`
|
||||
|
||||
async function onMouseEnter(ev: MouseEvent) {
|
||||
if (!ev.target) return
|
||||
currentHover?.classList.remove("focus")
|
||||
currentHover?.blur()
|
||||
const target = ev.target as HTMLInputElement
|
||||
currentHover = target
|
||||
currentHover.classList.add("focus")
|
||||
await displayPreview(target)
|
||||
}
|
||||
|
||||
async function onMouseLeave(ev: MouseEvent) {
|
||||
if (!ev.target) return
|
||||
const target = ev.target as HTMLElement
|
||||
target.classList.remove("focus")
|
||||
}
|
||||
|
||||
const events = [
|
||||
["mouseenter", onMouseEnter],
|
||||
["mouseleave", onMouseLeave],
|
||||
[
|
||||
"click",
|
||||
(event: MouseEvent) => {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
|
||||
hideSearch()
|
||||
},
|
||||
],
|
||||
] as const
|
||||
|
||||
events.forEach(([event, handler]) => {
|
||||
itemTile.addEventListener(event, handler)
|
||||
window.addCleanup(() => itemTile.removeEventListener(event, handler))
|
||||
})
|
||||
|
||||
return itemTile
|
||||
}
|
||||
|
||||
function displayResults(finalResults: Item[]) {
|
||||
async function displayResults(finalResults: Item[]) {
|
||||
if (!results) return
|
||||
|
||||
removeAllChildren(results)
|
||||
if (finalResults.length === 0) {
|
||||
results.innerHTML = `<a class="result-card">
|
||||
<h3>No results.</h3>
|
||||
<p>Try another search term?</p>
|
||||
</a>`
|
||||
results.innerHTML = `<a class="result-card no-match">
|
||||
<h3>No results.</h3>
|
||||
<p>Try another search term?</p>
|
||||
</a>`
|
||||
} else {
|
||||
results.append(...finalResults.map(resultToHTML))
|
||||
}
|
||||
|
||||
if (finalResults.length === 0 && preview) {
|
||||
// no results, clear previous preview
|
||||
removeAllChildren(preview)
|
||||
} else {
|
||||
// focus on first result, then also dispatch preview immediately
|
||||
const firstChild = results.firstElementChild as HTMLElement
|
||||
firstChild.classList.add("focus")
|
||||
currentHover = firstChild as HTMLInputElement
|
||||
await displayPreview(firstChild)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchContent(slug: FullSlug): Promise<Element[]> {
|
||||
if (fetchContentCache.has(slug)) {
|
||||
return fetchContentCache.get(slug) as Element[]
|
||||
}
|
||||
|
||||
const targetUrl = resolveUrl(slug).toString()
|
||||
const contents = await fetch(targetUrl)
|
||||
.then((res) => res.text())
|
||||
.then((contents) => {
|
||||
if (contents === undefined) {
|
||||
throw new Error(`Could not fetch ${targetUrl}`)
|
||||
}
|
||||
const html = p.parseFromString(contents ?? "", "text/html")
|
||||
normalizeRelativeURLs(html, targetUrl)
|
||||
return [...html.getElementsByClassName("popover-hint")]
|
||||
})
|
||||
|
||||
fetchContentCache.set(slug, contents)
|
||||
return contents
|
||||
}
|
||||
|
||||
async function displayPreview(el: HTMLElement | null) {
|
||||
if (!searchLayout || !enablePreview || !el || !preview) return
|
||||
const slug = el.id as FullSlug
|
||||
const innerDiv = await fetchContent(slug).then((contents) =>
|
||||
contents.flatMap((el) => [...highlightHTML(currentSearchTerm, el as HTMLElement).children]),
|
||||
)
|
||||
previewInner = document.createElement("div")
|
||||
previewInner.classList.add("preview-inner")
|
||||
previewInner.append(...innerDiv)
|
||||
preview.replaceChildren(previewInner)
|
||||
|
||||
// scroll to longest
|
||||
const highlights = [...preview.querySelectorAll(".highlight")].sort(
|
||||
(a, b) => b.innerHTML.length - a.innerHTML.length,
|
||||
)
|
||||
highlights[0]?.scrollIntoView({ block: "start" })
|
||||
}
|
||||
|
||||
async function onType(e: HTMLElementEventMap["input"]) {
|
||||
let term = (e.target as HTMLInputElement).value
|
||||
if (!searchLayout || !index) return
|
||||
currentSearchTerm = (e.target as HTMLInputElement).value
|
||||
searchLayout.classList.toggle("display-results", currentSearchTerm !== "")
|
||||
searchType = currentSearchTerm.startsWith("#") ? "tags" : "basic"
|
||||
|
||||
let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[]
|
||||
|
||||
if (term.toLowerCase().startsWith("#")) {
|
||||
searchType = "tags"
|
||||
} else {
|
||||
searchType = "basic"
|
||||
}
|
||||
|
||||
switch (searchType) {
|
||||
case "tags": {
|
||||
term = term.substring(1)
|
||||
searchResults =
|
||||
(await index?.searchAsync({ query: term, limit: numSearchResults, index: ["tags"] })) ??
|
||||
[]
|
||||
break
|
||||
}
|
||||
case "basic":
|
||||
default: {
|
||||
searchResults =
|
||||
(await index?.searchAsync({
|
||||
query: term,
|
||||
limit: numSearchResults,
|
||||
index: ["title", "content"],
|
||||
})) ?? []
|
||||
}
|
||||
if (searchType === "tags") {
|
||||
searchResults = await index.searchAsync({
|
||||
query: currentSearchTerm.substring(1),
|
||||
limit: numSearchResults,
|
||||
index: ["tags"],
|
||||
})
|
||||
} else if (searchType === "basic") {
|
||||
searchResults = await index.searchAsync({
|
||||
query: currentSearchTerm,
|
||||
limit: numSearchResults,
|
||||
index: ["title", "content"],
|
||||
})
|
||||
}
|
||||
|
||||
const getByField = (field: string): number[] => {
|
||||
@ -288,50 +453,19 @@ document.addEventListener("nav", async (e: unknown) => {
|
||||
...getByField("content"),
|
||||
...getByField("tags"),
|
||||
])
|
||||
const finalResults = [...allIds].map((id) => formatForDisplay(term, id))
|
||||
displayResults(finalResults)
|
||||
}
|
||||
|
||||
if (prevShortcutHandler) {
|
||||
document.removeEventListener("keydown", prevShortcutHandler)
|
||||
const finalResults = [...allIds].map((id) => formatForDisplay(currentSearchTerm, id))
|
||||
await displayResults(finalResults)
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", shortcutHandler)
|
||||
prevShortcutHandler = shortcutHandler
|
||||
searchIcon?.removeEventListener("click", () => showSearch("basic"))
|
||||
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
|
||||
searchIcon?.addEventListener("click", () => showSearch("basic"))
|
||||
searchBar?.removeEventListener("input", onType)
|
||||
window.addCleanup(() => searchIcon?.removeEventListener("click", () => showSearch("basic")))
|
||||
searchBar?.addEventListener("input", onType)
|
||||
window.addCleanup(() => searchBar?.removeEventListener("input", onType))
|
||||
|
||||
// setup index if it hasn't been already
|
||||
if (!index) {
|
||||
index = new FlexSearch.Document({
|
||||
charset: "latin:extra",
|
||||
encode: encoder,
|
||||
document: {
|
||||
id: "id",
|
||||
index: [
|
||||
{
|
||||
field: "title",
|
||||
tokenize: "forward",
|
||||
},
|
||||
{
|
||||
field: "content",
|
||||
tokenize: "forward",
|
||||
},
|
||||
{
|
||||
field: "tags",
|
||||
tokenize: "forward",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
fillDocument(index, data)
|
||||
}
|
||||
|
||||
// register handlers
|
||||
registerEscapeHandler(container, hideSearch)
|
||||
await fillDocument(data)
|
||||
})
|
||||
|
||||
/**
|
||||
@ -339,16 +473,20 @@ document.addEventListener("nav", async (e: unknown) => {
|
||||
* @param index index to fill
|
||||
* @param data data to fill index with
|
||||
*/
|
||||
async function fillDocument(index: FlexSearch.Document<Item, false>, data: any) {
|
||||
async function fillDocument(data: { [key: FullSlug]: ContentDetails }) {
|
||||
let id = 0
|
||||
const promises: Array<Promise<unknown>> = []
|
||||
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
||||
await index.addAsync(id, {
|
||||
id,
|
||||
slug: slug as FullSlug,
|
||||
title: fileData.title,
|
||||
content: fileData.content,
|
||||
tags: fileData.tags,
|
||||
})
|
||||
id++
|
||||
promises.push(
|
||||
index.addAsync(id++, {
|
||||
id,
|
||||
slug: slug as FullSlug,
|
||||
title: fileData.title,
|
||||
content: fileData.content,
|
||||
tags: fileData.tags,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return await Promise.all(promises)
|
||||
}
|
||||
|
||||
@ -39,6 +39,9 @@ function notifyNav(url: FullSlug) {
|
||||
document.dispatchEvent(event)
|
||||
}
|
||||
|
||||
const cleanupFns: Set<(...args: any[]) => void> = new Set()
|
||||
window.addCleanup = (fn) => cleanupFns.add(fn)
|
||||
|
||||
let p: DOMParser
|
||||
async function navigate(url: URL, isBack: boolean = false) {
|
||||
p = p || new DOMParser()
|
||||
@ -57,6 +60,10 @@ async function navigate(url: URL, isBack: boolean = false) {
|
||||
|
||||
if (!contents) return
|
||||
|
||||
// cleanup old
|
||||
cleanupFns.forEach((fn) => fn())
|
||||
cleanupFns.clear()
|
||||
|
||||
const html = p.parseFromString(contents, "text/html")
|
||||
normalizeRelativeURLs(html, url)
|
||||
|
||||
|
||||
@ -29,8 +29,8 @@ function setupToc() {
|
||||
const content = toc.nextElementSibling as HTMLElement | undefined
|
||||
if (!content) return
|
||||
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
|
||||
toc.removeEventListener("click", toggleToc)
|
||||
toc.addEventListener("click", toggleToc)
|
||||
window.addCleanup(() => toc.removeEventListener("click", toggleToc))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,10 +12,10 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb:
|
||||
cb()
|
||||
}
|
||||
|
||||
outsideContainer?.removeEventListener("click", click)
|
||||
outsideContainer?.addEventListener("click", click)
|
||||
document.removeEventListener("keydown", esc)
|
||||
window.addCleanup(() => outsideContainer?.removeEventListener("click", click))
|
||||
document.addEventListener("keydown", esc)
|
||||
window.addCleanup(() => document.removeEventListener("keydown", esc))
|
||||
}
|
||||
|
||||
export function removeAllChildren(node: HTMLElement) {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
@use "../../styles/variables.scss" as *;
|
||||
|
||||
button#explorer {
|
||||
all: unset;
|
||||
background-color: transparent;
|
||||
@ -85,7 +87,7 @@ svg {
|
||||
color: var(--secondary);
|
||||
font-family: var(--headerFont);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
font-weight: $boldWeight;
|
||||
line-height: 1.5rem;
|
||||
display: inline-block;
|
||||
}
|
||||
@ -110,7 +112,7 @@ svg {
|
||||
font-size: 0.95rem;
|
||||
display: inline-block;
|
||||
color: var(--secondary);
|
||||
font-weight: 600;
|
||||
font-weight: $boldWeight;
|
||||
margin: 0;
|
||||
line-height: 1.5rem;
|
||||
pointer-events: none;
|
||||
|
||||
@ -57,15 +57,11 @@
|
||||
}
|
||||
|
||||
& > #search-space {
|
||||
width: 50%;
|
||||
margin-top: 15vh;
|
||||
width: 65%;
|
||||
margin-top: 12vh;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
@media all and (max-width: $fullPageWidth) {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
@ -89,93 +85,133 @@
|
||||
}
|
||||
}
|
||||
|
||||
& > #results-container {
|
||||
& .result-card {
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
border: 1px solid var(--lightgray);
|
||||
border-bottom: none;
|
||||
width: 100%;
|
||||
& > #search-layout {
|
||||
display: none;
|
||||
flex-direction: row;
|
||||
border: 1px solid var(--lightgray);
|
||||
|
||||
&.display-results {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media all and (min-width: $tabletBreakpoint) {
|
||||
&[data-preview] {
|
||||
& .result-card > p.preview {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > div {
|
||||
// vh - #search-space.margin-top
|
||||
height: calc(75vh - 12vh);
|
||||
background: none;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-right: 1px solid var(--lightgray);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: $tabletBreakpoint) {
|
||||
display: block;
|
||||
& > *:not(#results-container) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
& > #results-container {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
& .highlight {
|
||||
background: color-mix(in srgb, var(--tertiary) 60%, transparent);
|
||||
border-radius: 5px;
|
||||
scroll-margin-top: 2rem;
|
||||
}
|
||||
|
||||
& > #preview-container {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
|
||||
// normalize card props
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
text-transform: none;
|
||||
text-align: left;
|
||||
color: var(--dark);
|
||||
line-height: 1.5em;
|
||||
font-weight: $normalWeight;
|
||||
background: var(--light);
|
||||
outline: none;
|
||||
font-weight: inherit;
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
|
||||
& .highlight {
|
||||
color: var(--secondary);
|
||||
font-weight: 700;
|
||||
& .preview-inner {
|
||||
margin: 0 auto;
|
||||
width: min($pageWidth, 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: var(--lightgray);
|
||||
}
|
||||
& > #results-container {
|
||||
overflow-y: auto;
|
||||
|
||||
&:first-of-type {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
& .result-card {
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
border-bottom: 1px solid var(--lightgray);
|
||||
}
|
||||
width: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
|
||||
& > h3 {
|
||||
// normalize card props
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
}
|
||||
text-transform: none;
|
||||
text-align: left;
|
||||
background: var(--light);
|
||||
outline: none;
|
||||
font-weight: inherit;
|
||||
|
||||
& > ul > li {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
&:focus,
|
||||
&.focus {
|
||||
background: var(--lightgray);
|
||||
}
|
||||
|
||||
& > ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
padding-left: 0;
|
||||
gap: 0.4rem;
|
||||
margin: 0;
|
||||
margin-top: 0.45rem;
|
||||
// Offset border radius
|
||||
margin-left: -2px;
|
||||
overflow: hidden;
|
||||
background-clip: border-box;
|
||||
}
|
||||
& > h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& > ul > li > p {
|
||||
border-radius: 8px;
|
||||
background-color: var(--highlight);
|
||||
overflow: hidden;
|
||||
background-clip: border-box;
|
||||
padding: 0.03rem 0.4rem;
|
||||
margin: 0;
|
||||
color: var(--secondary);
|
||||
opacity: 0.85;
|
||||
}
|
||||
& > ul.tags {
|
||||
margin-top: 0.45rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
& > ul > li > .match-tag {
|
||||
color: var(--tertiary);
|
||||
font-weight: bold;
|
||||
opacity: 1;
|
||||
}
|
||||
& > ul > li > p {
|
||||
border-radius: 8px;
|
||||
background-color: var(--highlight);
|
||||
padding: 0.2rem 0.4rem;
|
||||
margin: 0 0.1rem;
|
||||
line-height: 1.4rem;
|
||||
font-weight: $boldWeight;
|
||||
color: var(--secondary);
|
||||
|
||||
& > p {
|
||||
margin-bottom: 0;
|
||||
&.match-tag {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
& > p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,9 +131,11 @@ function addGlobalPageResources(
|
||||
componentResources.afterDOMLoaded.push(spaRouterScript)
|
||||
} else {
|
||||
componentResources.afterDOMLoaded.push(`
|
||||
window.spaNavigate = (url, _) => window.location.assign(url)
|
||||
const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
|
||||
document.dispatchEvent(event)`)
|
||||
window.spaNavigate = (url, _) => window.location.assign(url)
|
||||
window.addCleanup = () => {}
|
||||
const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
|
||||
document.dispatchEvent(event)
|
||||
`)
|
||||
}
|
||||
|
||||
let wsUrl = `ws://localhost:${ctx.argv.wsPort}`
|
||||
@ -147,9 +149,9 @@ function addGlobalPageResources(
|
||||
loadTime: "afterDOMReady",
|
||||
contentType: "inline",
|
||||
script: `
|
||||
const socket = new WebSocket('${wsUrl}')
|
||||
socket.addEventListener('message', () => document.location.reload())
|
||||
`,
|
||||
const socket = new WebSocket('${wsUrl}')
|
||||
socket.addEventListener('message', () => document.location.reload())
|
||||
`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import { escapeHTML } from "../../util/escape"
|
||||
import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path"
|
||||
import { QuartzEmitterPlugin } from "../types"
|
||||
import { toHtml } from "hast-util-to-html"
|
||||
import path from "path"
|
||||
import { write } from "./helpers"
|
||||
|
||||
export type ContentIndex = Map<FullSlug, ContentDetails>
|
||||
|
||||
@ -26,12 +26,12 @@ export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
|
||||
return {
|
||||
css: [
|
||||
// base css
|
||||
"https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css",
|
||||
"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css",
|
||||
],
|
||||
js: [
|
||||
{
|
||||
// fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md
|
||||
src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js",
|
||||
src: "https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/copy-tex.min.js",
|
||||
loadTime: "afterDOMReady",
|
||||
contentType: "external",
|
||||
},
|
||||
|
||||
@ -44,39 +44,7 @@ const defaultOptions: Options = {
|
||||
enableVideoEmbed: true,
|
||||
}
|
||||
|
||||
const icons = {
|
||||
infoIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>`,
|
||||
pencilIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="2" x2="22" y2="6"></line><path d="M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z"></path></svg>`,
|
||||
clipboardListIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><path d="M12 11h4"></path><path d="M12 16h4"></path><path d="M8 11h.01"></path><path d="M8 16h.01"></path></svg>`,
|
||||
checkCircleIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path><path d="m9 12 2 2 4-4"></path></svg>`,
|
||||
flameIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg>`,
|
||||
checkIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`,
|
||||
helpCircleIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`,
|
||||
alertTriangleIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`,
|
||||
xIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`,
|
||||
zapIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>`,
|
||||
bugIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="8" height="14" x="8" y="6" rx="4"></rect><path d="m19 7-3 2"></path><path d="m5 7 3 2"></path><path d="m19 19-3-2"></path><path d="m5 19 3-2"></path><path d="M20 13h-4"></path><path d="M4 13h4"></path><path d="m10 4 1 2"></path><path d="m14 4-1 2"></path></svg>`,
|
||||
listIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>`,
|
||||
quoteIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"></path><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"></path></svg>`,
|
||||
}
|
||||
|
||||
const callouts = {
|
||||
note: icons.pencilIcon,
|
||||
abstract: icons.clipboardListIcon,
|
||||
info: icons.infoIcon,
|
||||
todo: icons.checkCircleIcon,
|
||||
tip: icons.flameIcon,
|
||||
success: icons.checkIcon,
|
||||
question: icons.helpCircleIcon,
|
||||
warning: icons.alertTriangleIcon,
|
||||
failure: icons.xIcon,
|
||||
danger: icons.zapIcon,
|
||||
bug: icons.bugIcon,
|
||||
example: icons.listIcon,
|
||||
quote: icons.quoteIcon,
|
||||
}
|
||||
|
||||
const calloutMapping: Record<string, keyof typeof callouts> = {
|
||||
const calloutMapping = {
|
||||
note: "note",
|
||||
abstract: "abstract",
|
||||
summary: "abstract",
|
||||
@ -104,12 +72,12 @@ const calloutMapping: Record<string, keyof typeof callouts> = {
|
||||
example: "example",
|
||||
quote: "quote",
|
||||
cite: "quote",
|
||||
}
|
||||
} as const
|
||||
|
||||
function canonicalizeCallout(calloutName: string): keyof typeof callouts {
|
||||
let callout = calloutName.toLowerCase() as keyof typeof calloutMapping
|
||||
function canonicalizeCallout(calloutName: string): keyof typeof calloutMapping {
|
||||
const normalizedCallout = calloutName.toLowerCase() as keyof typeof calloutMapping
|
||||
// if callout is not recognized, make it a custom one
|
||||
return calloutMapping[callout] ?? calloutName
|
||||
return calloutMapping[normalizedCallout] ?? calloutName
|
||||
}
|
||||
|
||||
export const externalLinkRegex = /^https?:\/\//i
|
||||
@ -322,8 +290,9 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||
}
|
||||
|
||||
tag = slugTag(tag)
|
||||
if (file.data.frontmatter?.tags?.includes(tag)) {
|
||||
file.data.frontmatter.tags.push(tag)
|
||||
if (file.data.frontmatter) {
|
||||
const noteTags = file.data.frontmatter.tags ?? []
|
||||
file.data.frontmatter.tags = [...new Set([...noteTags, tag])]
|
||||
}
|
||||
|
||||
return {
|
||||
@ -411,32 +380,31 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
||||
const match = firstLine.match(calloutRegex)
|
||||
if (match && match.input) {
|
||||
const [calloutDirective, typeString, collapseChar] = match
|
||||
const calloutType = canonicalizeCallout(
|
||||
typeString.toLowerCase() as keyof typeof calloutMapping,
|
||||
)
|
||||
const calloutType = canonicalizeCallout(typeString.toLowerCase())
|
||||
const collapse = collapseChar === "+" || collapseChar === "-"
|
||||
const defaultState = collapseChar === "-" ? "collapsed" : "expanded"
|
||||
const titleContent =
|
||||
match.input.slice(calloutDirective.length).trim() || capitalize(calloutType)
|
||||
const titleContent = match.input.slice(calloutDirective.length).trim()
|
||||
const useDefaultTitle = titleContent === "" && restOfTitle.length === 0
|
||||
const titleNode: Paragraph = {
|
||||
type: "paragraph",
|
||||
children:
|
||||
restOfTitle.length === 0
|
||||
? [{ type: "text", value: titleContent + " " }]
|
||||
: restOfTitle,
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
value: useDefaultTitle ? capitalize(calloutType) : titleContent + " ",
|
||||
},
|
||||
...restOfTitle,
|
||||
],
|
||||
}
|
||||
const title = mdastToHtml(titleNode)
|
||||
|
||||
const toggleIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="fold">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>`
|
||||
const toggleIcon = `<div class="fold-callout-icon"></div>`
|
||||
|
||||
const titleHtml: Html = {
|
||||
type: "html",
|
||||
value: `<div
|
||||
class="callout-title"
|
||||
>
|
||||
<div class="callout-icon">${callouts[calloutType] ?? callouts.note}</div>
|
||||
<div class="callout-icon"></div>
|
||||
<div class="callout-title-inner">${title}</div>
|
||||
${collapse ? toggleIcon : ""}
|
||||
</div>`,
|
||||
|
||||
@ -30,7 +30,7 @@ section {
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: color-mix(in srgb, var(--tertiary) 75%, transparent);
|
||||
background: color-mix(in srgb, var(--tertiary) 60%, transparent);
|
||||
color: var(--darkgray);
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ ul,
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 600;
|
||||
font-weight: $boldWeight;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
color: var(--secondary);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
@use "./variables.scss" as *;
|
||||
@use "sass:color";
|
||||
|
||||
.callout {
|
||||
@ -13,16 +14,33 @@
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
--callout-icon-note: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="2" x2="22" y2="6"></line><path d="M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z"></path></svg>');
|
||||
--callout-icon-abstract: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><path d="M12 11h4"></path><path d="M12 16h4"></path><path d="M8 11h.01"></path><path d="M8 16h.01"></path></svg>');
|
||||
--callout-icon-info: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>');
|
||||
--callout-icon-todo: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path><path d="m9 12 2 2 4-4"></path></svg>');
|
||||
--callout-icon-tip: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg> ');
|
||||
--callout-icon-success: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg> ');
|
||||
--callout-icon-question: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> ');
|
||||
--callout-icon-warning: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>');
|
||||
--callout-icon-failure: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> ');
|
||||
--callout-icon-danger: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg> ');
|
||||
--callout-icon-bug: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="8" height="14" x="8" y="6" rx="4"></rect><path d="m19 7-3 2"></path><path d="m5 7 3 2"></path><path d="m19 19-3-2"></path><path d="m5 19 3-2"></path><path d="M20 13h-4"></path><path d="M4 13h4"></path><path d="m10 4 1 2"></path><path d="m14 4-1 2"></path></svg>');
|
||||
--callout-icon-example: url('data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg> ');
|
||||
--callout-icon-quote: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"></path><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"></path></svg>');
|
||||
--callout-icon-fold: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpolyline points="6 9 12 15 18 9"%3E%3C/polyline%3E%3C/svg%3E');
|
||||
|
||||
&[data-callout] {
|
||||
--color: #448aff;
|
||||
--border: #448aff44;
|
||||
--bg: #448aff10;
|
||||
--callout-icon: var(--callout-icon-note);
|
||||
}
|
||||
|
||||
&[data-callout="abstract"] {
|
||||
--color: #00b0ff;
|
||||
--border: #00b0ff44;
|
||||
--bg: #00b0ff10;
|
||||
--callout-icon: var(--callout-icon-abstract);
|
||||
}
|
||||
|
||||
&[data-callout="info"],
|
||||
@ -30,30 +48,39 @@
|
||||
--color: #00b8d4;
|
||||
--border: #00b8d444;
|
||||
--bg: #00b8d410;
|
||||
--callout-icon: var(--callout-icon-info);
|
||||
}
|
||||
|
||||
&[data-callout="todo"] {
|
||||
--callout-icon: var(--callout-icon-todo);
|
||||
}
|
||||
|
||||
&[data-callout="tip"] {
|
||||
--color: #00bfa5;
|
||||
--border: #00bfa544;
|
||||
--bg: #00bfa510;
|
||||
--callout-icon: var(--callout-icon-tip);
|
||||
}
|
||||
|
||||
&[data-callout="success"] {
|
||||
--color: #09ad7a;
|
||||
--border: #09ad7144;
|
||||
--bg: #09ad7110;
|
||||
--callout-icon: var(--callout-icon-success);
|
||||
}
|
||||
|
||||
&[data-callout="question"] {
|
||||
--color: #dba642;
|
||||
--border: #dba64244;
|
||||
--bg: #dba64210;
|
||||
--callout-icon: var(--callout-icon-question);
|
||||
}
|
||||
|
||||
&[data-callout="warning"] {
|
||||
--color: #db8942;
|
||||
--border: #db894244;
|
||||
--bg: #db894210;
|
||||
--callout-icon: var(--callout-icon-warning);
|
||||
}
|
||||
|
||||
&[data-callout="failure"],
|
||||
@ -62,50 +89,74 @@
|
||||
--color: #db4242;
|
||||
--border: #db424244;
|
||||
--bg: #db424210;
|
||||
--callout-icon: var(--callout-icon-failure);
|
||||
}
|
||||
|
||||
&[data-callout="bug"] {
|
||||
--callout-icon: var(--callout-icon-bug);
|
||||
}
|
||||
|
||||
&[data-callout="danger"] {
|
||||
--callout-icon: var(--callout-icon-danger);
|
||||
}
|
||||
|
||||
&[data-callout="example"] {
|
||||
--color: #7a43b5;
|
||||
--border: #7a43b544;
|
||||
--bg: #7a43b510;
|
||||
--callout-icon: var(--callout-icon-example);
|
||||
}
|
||||
|
||||
&[data-callout="quote"] {
|
||||
--color: var(--secondary);
|
||||
--border: var(--lightgray);
|
||||
--callout-icon: var(--callout-icon-quote);
|
||||
}
|
||||
|
||||
&.is-collapsed > .callout-title > .fold {
|
||||
&.is-collapsed > .callout-title > .fold-callout-icon {
|
||||
transform: rotateZ(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.callout-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 1rem 0;
|
||||
color: var(--color);
|
||||
|
||||
& .fold {
|
||||
margin-left: 0.5rem;
|
||||
transition: transform 0.3s ease;
|
||||
--icon-size: 18px;
|
||||
|
||||
& .fold-callout-icon {
|
||||
transition: transform 0.15s ease;
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
--callout-icon: var(--callout-icon-fold);
|
||||
}
|
||||
|
||||
& > .callout-title-inner > p {
|
||||
color: var(--color);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.callout-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 18px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
.callout-icon,
|
||||
& .fold-callout-icon {
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
|
||||
.callout-title-inner {
|
||||
font-weight: 700;
|
||||
// icon support
|
||||
background-size: var(--icon-size) var(--icon-size);
|
||||
background-position: center;
|
||||
background-color: var(--color);
|
||||
mask-image: var(--callout-icon);
|
||||
mask-size: var(--icon-size) var(--icon-size);
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.callout-title-inner {
|
||||
font-weight: $boldWeight;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
$pageWidth: 750px;
|
||||
$mobileBreakpoint: 600px;
|
||||
$tabletBreakpoint: 1200px;
|
||||
$tabletBreakpoint: 1000px;
|
||||
$sidePanelWidth: 380px;
|
||||
$topSpacing: 6rem;
|
||||
$fullPageWidth: $pageWidth + 2 * $sidePanelWidth;
|
||||
$boldWeight: 700;
|
||||
$normalWeight: 400;
|
||||
|
||||
@ -13,8 +13,8 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact"
|
||||
"jsxImportSource": "preact",
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "./package.json"],
|
||||
"exclude": ["build/**/*.d.ts"]
|
||||
"exclude": ["build/**/*.d.ts"],
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user