mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-01 02:07:55 +01:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34a8dfcd55 | ||
|
|
44da82467e | ||
|
|
3231ce6e79 | ||
|
|
a0b927da4a | ||
|
|
5ab922f316 | ||
|
|
d11a0e71a8 | ||
|
|
2b57a68e1f | ||
|
|
18cd58617d | ||
|
|
ee868b2d79 | ||
|
|
5a36e5b68d | ||
|
|
0416c03ae6 | ||
|
|
3b596c9311 | ||
|
|
970a30a139 | ||
|
|
dc62aeb213 | ||
|
|
9b8e0c9d1a | ||
|
|
45b93a80f4 | ||
|
|
e9fb0ecb96 | ||
|
|
c0c0b24138 | ||
|
|
c00089bd57 | ||
|
|
8a6ebd1939 | ||
|
|
f78b512436 | ||
|
|
295b8fc914 | ||
|
|
756acc7f97 | ||
|
|
9aa6a18be2 | ||
|
|
444e05ee21 | ||
|
|
1c175b2d09 | ||
|
|
7b2ce8b4a3 | ||
|
|
f2e93c3314 | ||
|
|
25e6869d38 | ||
|
|
bfd877133b | ||
|
|
422986c98b | ||
|
|
75d64eac91 | ||
|
|
355aa22318 | ||
|
|
7cb1c291c8 |
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -8,4 +8,4 @@ updates:
|
|||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "weekly"
|
||||||
|
|||||||
@@ -156,12 +156,13 @@ document.addEventListener("nav", () => {
|
|||||||
// do page specific logic here
|
// do page specific logic here
|
||||||
// e.g. attach event listeners
|
// e.g. attach event listeners
|
||||||
const toggleSwitch = document.querySelector("#switch") as HTMLInputElement
|
const toggleSwitch = document.querySelector("#switch") as HTMLInputElement
|
||||||
toggleSwitch.removeEventListener("change", switchTheme)
|
|
||||||
toggleSwitch.addEventListener("change", switchTheme)
|
toggleSwitch.addEventListener("change", switchTheme)
|
||||||
|
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
#### Importing Code
|
||||||
|
|
||||||
|
|||||||
@@ -32,11 +32,12 @@ By default, custom callouts are handled by applying the `note` style. To make fa
|
|||||||
|
|
||||||
```scss title="quartz/styles/custom.scss"
|
```scss title="quartz/styles/custom.scss"
|
||||||
.callout {
|
.callout {
|
||||||
&[data-callout="custom"] {
|
&[data-callout="custom"] {
|
||||||
--color: #customcolor;
|
--color: #customcolor;
|
||||||
--border: #custombordercolor;
|
--border: #custombordercolor;
|
||||||
--bg: #custombg;
|
--bg: #custombg;
|
||||||
--callout-icon: url('data:image/svg+xml; utf8, <custom formatted svg>'); //SVG icon code
|
--callout-icon: url("data:image/svg+xml; utf8, <custom formatted svg>"); //SVG icon code
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ By default, custom callouts are handled by applying the `note` style. To make fa
|
|||||||
> [!info]
|
> [!info]
|
||||||
> Default title
|
> Default title
|
||||||
|
|
||||||
> [!question]+ Can callouts be nested?
|
> [!question]+ Can callouts be _nested_?
|
||||||
>
|
>
|
||||||
> > [!todo]- Yes!, they can. And collapsed!
|
> > [!todo]- Yes!, they can. And collapsed!
|
||||||
> >
|
> >
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Recent Notes
|
|||||||
tags: component
|
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
|
## Customization
|
||||||
|
|
||||||
|
|||||||
1
globals.d.ts
vendored
1
globals.d.ts
vendored
@@ -8,5 +8,6 @@ export declare global {
|
|||||||
}
|
}
|
||||||
interface Window {
|
interface Window {
|
||||||
spaNavigate(url: URL, isBack: boolean = false)
|
spaNavigate(url: URL, isBack: boolean = false)
|
||||||
|
addCleanup(fn: (...args: any[]) => void)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
package-lock.json
generated
39
package-lock.json
generated
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "@jackyzha0/quartz",
|
"name": "@jackyzha0/quartz",
|
||||||
"version": "4.1.6",
|
"version": "4.2.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@jackyzha0/quartz",
|
"name": "@jackyzha0/quartz",
|
||||||
"version": "4.1.6",
|
"version": "4.2.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.7.0",
|
"@clack/prompts": "^0.7.0",
|
||||||
"@floating-ui/dom": "^1.6.1",
|
"@floating-ui/dom": "^1.6.1",
|
||||||
"@napi-rs/simple-git": "0.1.14",
|
"@napi-rs/simple-git": "0.1.14",
|
||||||
"async-mutex": "^0.4.0",
|
"async-mutex": "^0.4.1",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"cli-spinner": "^0.2.10",
|
"cli-spinner": "^0.2.10",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lightningcss": "^1.23.0",
|
"lightningcss": "^1.23.0",
|
||||||
"mdast-util-find-and-replace": "^3.0.1",
|
"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",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"micromorph": "^0.4.5",
|
"micromorph": "^0.4.5",
|
||||||
"preact": "^10.19.3",
|
"preact": "^10.19.3",
|
||||||
@@ -71,9 +71,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cli-spinner": "^0.2.3",
|
"@types/cli-spinner": "^0.2.3",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/hast": "^3.0.3",
|
"@types/hast": "^3.0.4",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^20.11.11",
|
"@types/node": "^20.11.14",
|
||||||
"@types/pretty-time": "^1.1.5",
|
"@types/pretty-time": "^1.1.5",
|
||||||
"@types/source-map-support": "^0.5.10",
|
"@types/source-map-support": "^0.5.10",
|
||||||
"@types/ws": "^8.5.10",
|
"@types/ws": "^8.5.10",
|
||||||
@@ -1043,9 +1043,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/hast": {
|
"node_modules/@types/hast": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||||
"integrity": "sha512-2fYGlaDy/qyLlhidX42wAH0KBi2TCjKMH8CHmBXgRlJ3Y+OXTiqsPQ6IWarZKwF1JoUcAJdPogv1d4b0COTpmQ==",
|
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/unist": "*"
|
"@types/unist": "*"
|
||||||
}
|
}
|
||||||
@@ -1088,9 +1088,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.11.11",
|
"version": "20.11.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.14.tgz",
|
||||||
"integrity": "sha512-PlJCXfb57Jrman0H1BxO2+Q7qwih2Mwk7T6Gvixj+SK4mqs4RWOGMMoP6p/LFa3UrP2CZOO6ai6otd7J/TB6Ug==",
|
"integrity": "sha512-w3yWCcwULefjP9DmDDsgUskrMoOy5Z8MiwKHr1FvqGPtx7CvJzQvxD7eKpxNtklQxLruxSXWddyeRtyud0RcXQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
@@ -1208,9 +1208,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/async-mutex": {
|
"node_modules/async-mutex": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz",
|
||||||
"integrity": "sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==",
|
"integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
@@ -3610,9 +3610,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mdast-util-to-hast": {
|
"node_modules/mdast-util-to-hast": {
|
||||||
"version": "13.0.2",
|
"version": "13.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz",
|
||||||
"integrity": "sha512-U5I+500EOOw9e3ZrclN3Is3fRpw8c19SMyNZlZ2IS+7vLsNzb2Om11VpIVOR+/0137GhZsFEF6YiKD5+0Hr2Og==",
|
"integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/hast": "^3.0.0",
|
"@types/hast": "^3.0.0",
|
||||||
"@types/mdast": "^4.0.0",
|
"@types/mdast": "^4.0.0",
|
||||||
@@ -3621,7 +3621,8 @@
|
|||||||
"micromark-util-sanitize-uri": "^2.0.0",
|
"micromark-util-sanitize-uri": "^2.0.0",
|
||||||
"trim-lines": "^3.0.0",
|
"trim-lines": "^3.0.0",
|
||||||
"unist-util-position": "^5.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": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -2,7 +2,7 @@
|
|||||||
"name": "@jackyzha0/quartz",
|
"name": "@jackyzha0/quartz",
|
||||||
"description": "🌱 publish your digital garden and notes as a website",
|
"description": "🌱 publish your digital garden and notes as a website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "4.1.6",
|
"version": "4.2.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"@clack/prompts": "^0.7.0",
|
"@clack/prompts": "^0.7.0",
|
||||||
"@floating-ui/dom": "^1.6.1",
|
"@floating-ui/dom": "^1.6.1",
|
||||||
"@napi-rs/simple-git": "0.1.14",
|
"@napi-rs/simple-git": "0.1.14",
|
||||||
"async-mutex": "^0.4.0",
|
"async-mutex": "^0.4.1",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"cli-spinner": "^0.2.10",
|
"cli-spinner": "^0.2.10",
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lightningcss": "^1.23.0",
|
"lightningcss": "^1.23.0",
|
||||||
"mdast-util-find-and-replace": "^3.0.1",
|
"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",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"micromorph": "^0.4.5",
|
"micromorph": "^0.4.5",
|
||||||
"preact": "^10.19.3",
|
"preact": "^10.19.3",
|
||||||
@@ -93,9 +93,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cli-spinner": "^0.2.3",
|
"@types/cli-spinner": "^0.2.3",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/hast": "^3.0.3",
|
"@types/hast": "^3.0.4",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^20.11.11",
|
"@types/node": "^20.11.14",
|
||||||
"@types/pretty-time": "^1.1.5",
|
"@types/pretty-time": "^1.1.5",
|
||||||
"@types/source-map-support": "^0.5.10",
|
"@types/source-map-support": "^0.5.10",
|
||||||
"@types/ws": "^8.5.10",
|
"@types/ws": "^8.5.10",
|
||||||
|
|||||||
@@ -126,17 +126,8 @@ async function rebuildFromEntrypoint(
|
|||||||
clientRefresh: () => void,
|
clientRefresh: () => void,
|
||||||
buildData: BuildData, // note: this function mutates buildData
|
buildData: BuildData, // note: this function mutates buildData
|
||||||
) {
|
) {
|
||||||
const {
|
const { ctx, ignored, mut, initialSlugs, contentMap, toRebuild, toRemove, trackedAssets } =
|
||||||
ctx,
|
buildData
|
||||||
ignored,
|
|
||||||
mut,
|
|
||||||
initialSlugs,
|
|
||||||
contentMap,
|
|
||||||
toRebuild,
|
|
||||||
toRemove,
|
|
||||||
trackedAssets,
|
|
||||||
lastBuildMs,
|
|
||||||
} = buildData
|
|
||||||
|
|
||||||
const { argv } = ctx
|
const { argv } = ctx
|
||||||
|
|
||||||
@@ -164,12 +155,12 @@ async function rebuildFromEntrypoint(
|
|||||||
toRemove.add(filePath)
|
toRemove.add(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// debounce rebuilds every 250ms
|
|
||||||
|
|
||||||
const buildStart = new Date().getTime()
|
const buildStart = new Date().getTime()
|
||||||
buildData.lastBuildMs = buildStart
|
buildData.lastBuildMs = buildStart
|
||||||
const release = await mut.acquire()
|
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()
|
release()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
function toggleCallout(this: HTMLElement) {
|
function toggleCallout(this: HTMLElement) {
|
||||||
const outerBlock = this.parentElement!
|
const outerBlock = this.parentElement!
|
||||||
outerBlock.classList.toggle(`is-collapsed`)
|
outerBlock.classList.toggle("is-collapsed")
|
||||||
const collapsed = outerBlock.classList.contains(`is-collapsed`)
|
const collapsed = outerBlock.classList.contains("is-collapsed")
|
||||||
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
|
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
|
||||||
outerBlock.style.maxHeight = height + `px`
|
outerBlock.style.maxHeight = height + "px"
|
||||||
|
|
||||||
// walk and adjust height of all parents
|
// walk and adjust height of all parents
|
||||||
let current = outerBlock
|
let current = outerBlock
|
||||||
let parent = outerBlock.parentElement
|
let parent = outerBlock.parentElement
|
||||||
while (parent) {
|
while (parent) {
|
||||||
if (!parent.classList.contains(`callout`)) {
|
if (!parent.classList.contains("callout")) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const collapsed = parent.classList.contains(`is-collapsed`)
|
const collapsed = parent.classList.contains("is-collapsed")
|
||||||
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
|
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
|
||||||
parent.style.maxHeight = height + `px`
|
parent.style.maxHeight = height + "px"
|
||||||
|
|
||||||
current = parent
|
current = parent
|
||||||
parent = parent.parentElement
|
parent = parent.parentElement
|
||||||
@@ -30,15 +30,15 @@ function setupCallout() {
|
|||||||
const title = div.firstElementChild
|
const title = div.firstElementChild
|
||||||
|
|
||||||
if (title) {
|
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
|
const height = collapsed ? title.scrollHeight : div.scrollHeight
|
||||||
div.style.maxHeight = height + `px`
|
div.style.maxHeight = height + "px"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener(`nav`, setupCallout)
|
document.addEventListener("nav", setupCallout)
|
||||||
window.addEventListener(`resize`, setupCallout)
|
window.addEventListener("resize", setupCallout)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ document.addEventListener("nav", () => {
|
|||||||
button.type = "button"
|
button.type = "button"
|
||||||
button.innerHTML = svgCopy
|
button.innerHTML = svgCopy
|
||||||
button.ariaLabel = "Copy source"
|
button.ariaLabel = "Copy source"
|
||||||
button.addEventListener("click", () => {
|
function onClick() {
|
||||||
navigator.clipboard.writeText(source).then(
|
navigator.clipboard.writeText(source).then(
|
||||||
() => {
|
() => {
|
||||||
button.blur()
|
button.blur()
|
||||||
@@ -26,7 +26,9 @@ document.addEventListener("nav", () => {
|
|||||||
},
|
},
|
||||||
(error) => console.error(error),
|
(error) => console.error(error),
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
button.addEventListener("click", onClick)
|
||||||
|
window.addCleanup(() => button.removeEventListener("click", onClick))
|
||||||
els[i].prepend(button)
|
els[i].prepend(button)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,28 +10,31 @@ const emitThemeChangeEvent = (theme: "light" | "dark") => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
const switchTheme = (e: any) => {
|
const switchTheme = (e: Event) => {
|
||||||
const newTheme = e.target.checked ? "dark" : "light"
|
const newTheme = (e.target as HTMLInputElement)?.checked ? "dark" : "light"
|
||||||
document.documentElement.setAttribute("saved-theme", newTheme)
|
document.documentElement.setAttribute("saved-theme", newTheme)
|
||||||
localStorage.setItem("theme", newTheme)
|
localStorage.setItem("theme", newTheme)
|
||||||
emitThemeChangeEvent(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
|
// Darkmode toggle
|
||||||
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
|
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
|
||||||
toggleSwitch.removeEventListener("change", switchTheme)
|
|
||||||
toggleSwitch.addEventListener("change", switchTheme)
|
toggleSwitch.addEventListener("change", switchTheme)
|
||||||
|
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
|
||||||
if (currentTheme === "dark") {
|
if (currentTheme === "dark") {
|
||||||
toggleSwitch.checked = true
|
toggleSwitch.checked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for changes in prefers-color-scheme
|
// Listen for changes in prefers-color-scheme
|
||||||
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
colorSchemeMediaQuery.addEventListener("change", (e) => {
|
colorSchemeMediaQuery.addEventListener("change", themeChange)
|
||||||
const newTheme = e.matches ? "dark" : "light"
|
window.addCleanup(() => colorSchemeMediaQuery.removeEventListener("change", themeChange))
|
||||||
document.documentElement.setAttribute("saved-theme", newTheme)
|
|
||||||
localStorage.setItem("theme", newTheme)
|
|
||||||
toggleSwitch.checked = e.matches
|
|
||||||
emitThemeChangeEvent(newTheme)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -57,20 +57,20 @@ function setupExplorer() {
|
|||||||
for (const item of document.getElementsByClassName(
|
for (const item of document.getElementsByClassName(
|
||||||
"folder-button",
|
"folder-button",
|
||||||
) as HTMLCollectionOf<HTMLElement>) {
|
) as HTMLCollectionOf<HTMLElement>) {
|
||||||
item.removeEventListener("click", toggleFolder)
|
|
||||||
item.addEventListener("click", toggleFolder)
|
item.addEventListener("click", toggleFolder)
|
||||||
|
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
explorer.removeEventListener("click", toggleExplorer)
|
|
||||||
explorer.addEventListener("click", toggleExplorer)
|
explorer.addEventListener("click", toggleExplorer)
|
||||||
|
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
|
||||||
|
|
||||||
// Set up click handlers for each folder (click handler on folder "icon")
|
// Set up click handlers for each folder (click handler on folder "icon")
|
||||||
for (const item of document.getElementsByClassName(
|
for (const item of document.getElementsByClassName(
|
||||||
"folder-icon",
|
"folder-icon",
|
||||||
) as HTMLCollectionOf<HTMLElement>) {
|
) as HTMLCollectionOf<HTMLElement>) {
|
||||||
item.removeEventListener("click", toggleFolder)
|
|
||||||
item.addEventListener("click", toggleFolder)
|
item.addEventListener("click", toggleFolder)
|
||||||
|
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get folder state from local storage
|
// Get folder state from local storage
|
||||||
|
|||||||
@@ -325,6 +325,6 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
await renderGraph("graph-container", slug)
|
await renderGraph("graph-container", slug)
|
||||||
|
|
||||||
const containerIcon = document.getElementById("global-graph-icon")
|
const containerIcon = document.getElementById("global-graph-icon")
|
||||||
containerIcon?.removeEventListener("click", renderGlobalGraph)
|
|
||||||
containerIcon?.addEventListener("click", renderGlobalGraph)
|
containerIcon?.addEventListener("click", renderGlobalGraph)
|
||||||
|
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ async function mouseEnterHandler(
|
|||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
|
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
|
||||||
for (const link of links) {
|
for (const link of links) {
|
||||||
link.removeEventListener("mouseenter", mouseEnterHandler)
|
|
||||||
link.addEventListener("mouseenter", mouseEnterHandler)
|
link.addEventListener("mouseenter", mouseEnterHandler)
|
||||||
|
window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,23 +11,53 @@ interface Item {
|
|||||||
tags: string[]
|
tags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
let index: FlexSearch.Document<Item> | undefined = undefined
|
|
||||||
|
|
||||||
// Can be expanded with things like "term" in the future
|
// Can be expanded with things like "term" in the future
|
||||||
type SearchType = "basic" | "tags"
|
type SearchType = "basic" | "tags"
|
||||||
|
|
||||||
// Current searchType
|
|
||||||
let searchType: SearchType = "basic"
|
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 contextWindowWords = 30
|
||||||
const numSearchResults = 8
|
const numSearchResults = 8
|
||||||
const numTagResults = 5
|
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) {
|
function highlight(searchTerm: string, text: string, trim?: boolean) {
|
||||||
// try to highlight longest tokens first
|
const tokenizedTerms = tokenizeTerm(searchTerm)
|
||||||
const tokenizedTerms = searchTerm
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter((t) => t !== "")
|
|
||||||
.sort((a, b) => b.length - a.length)
|
|
||||||
let tokenizedText = text.split(/\s+/).filter((t) => t !== "")
|
let tokenizedText = text.split(/\s+/).filter((t) => t !== "")
|
||||||
|
|
||||||
let startIndex = 0
|
let startIndex = 0
|
||||||
@@ -71,15 +101,49 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
|
|||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const p = new DOMParser()
|
function highlightHTML(searchTerm: string, el: HTMLElement) {
|
||||||
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
const p = new DOMParser()
|
||||||
let prevShortcutHandler: ((e: HTMLElementEventMap["keydown"]) => void) | undefined = undefined
|
const tokenizedTerms = tokenizeTerm(searchTerm)
|
||||||
|
const html = p.parseFromString(el.innerHTML, "text/html")
|
||||||
|
|
||||||
const fetchContentCache: Map<FullSlug, Element[]> = new Map()
|
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"]) => {
|
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||||
const currentSlug = e.detail.url
|
const currentSlug = e.detail.url
|
||||||
|
|
||||||
const data = await fetchData
|
const data = await fetchData
|
||||||
const container = document.getElementById("search-container")
|
const container = document.getElementById("search-container")
|
||||||
const sidebar = container?.closest(".sidebar") as HTMLElement
|
const sidebar = container?.closest(".sidebar") as HTMLElement
|
||||||
@@ -96,15 +160,16 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
|
|
||||||
const enablePreview = searchLayout?.dataset?.preview === "true"
|
const enablePreview = searchLayout?.dataset?.preview === "true"
|
||||||
let preview: HTMLDivElement | undefined = undefined
|
let preview: HTMLDivElement | undefined = undefined
|
||||||
|
let previewInner: HTMLDivElement | undefined = undefined
|
||||||
const results = document.createElement("div")
|
const results = document.createElement("div")
|
||||||
results.id = "results-container"
|
results.id = "results-container"
|
||||||
results.style.flexBasis = enablePreview ? "30%" : "100%"
|
results.style.flexBasis = enablePreview ? "min(30%, 450px)" : "100%"
|
||||||
appendLayout(results)
|
appendLayout(results)
|
||||||
|
|
||||||
if (enablePreview) {
|
if (enablePreview) {
|
||||||
preview = document.createElement("div")
|
preview = document.createElement("div")
|
||||||
preview.id = "preview-container"
|
preview.id = "preview-container"
|
||||||
preview.style.flexBasis = "70%"
|
preview.style.flexBasis = "100%"
|
||||||
appendLayout(preview)
|
appendLayout(preview)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +187,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
if (preview) {
|
if (preview) {
|
||||||
removeAllChildren(preview)
|
removeAllChildren(preview)
|
||||||
}
|
}
|
||||||
|
if (searchLayout) {
|
||||||
|
searchLayout.classList.remove("display-results")
|
||||||
|
}
|
||||||
|
|
||||||
searchType = "basic" // reset search type after closing
|
searchType = "basic" // reset search type after closing
|
||||||
}
|
}
|
||||||
@@ -135,11 +203,14 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
searchBar?.focus()
|
searchBar?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentHover: HTMLInputElement | null = null
|
||||||
|
|
||||||
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
||||||
if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const searchBarOpen = container?.classList.contains("active")
|
const searchBarOpen = container?.classList.contains("active")
|
||||||
searchBarOpen ? hideSearch() : showSearch("basic")
|
searchBarOpen ? hideSearch() : showSearch("basic")
|
||||||
|
return
|
||||||
} else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
|
} else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
|
||||||
// Hotkey to open tag search
|
// Hotkey to open tag search
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -148,120 +219,94 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
|
|
||||||
// add "#" prefix for tag search
|
// add "#" prefix for tag search
|
||||||
if (searchBar) searchBar.value = "#"
|
if (searchBar) searchBar.value = "#"
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultCards = document.getElementsByClassName("result-card")
|
if (currentHover) {
|
||||||
|
currentHover.classList.remove("focus")
|
||||||
|
currentHover.blur()
|
||||||
|
}
|
||||||
|
|
||||||
// If search is active, then we will render the first result and display accordingly
|
// If search is active, then we will render the first result and display accordingly
|
||||||
if (!container?.classList.contains("active")) return
|
if (!container?.classList.contains("active")) return
|
||||||
else if (results?.contains(document.activeElement)) {
|
else if (e.key === "Enter") {
|
||||||
const active = document.activeElement as HTMLInputElement
|
// If result has focus, navigate to that one, otherwise pick first result
|
||||||
await displayPreview(active)
|
if (results?.contains(document.activeElement)) {
|
||||||
if (e.key === "Enter") {
|
const active = document.activeElement as HTMLInputElement
|
||||||
|
if (active.classList.contains("no-match")) return
|
||||||
|
await displayPreview(active)
|
||||||
active.click()
|
active.click()
|
||||||
|
} else {
|
||||||
|
const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null
|
||||||
|
if (!anchor || anchor?.classList.contains("no-match")) return
|
||||||
|
await displayPreview(anchor)
|
||||||
|
anchor.click()
|
||||||
}
|
}
|
||||||
} else {
|
} else if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) {
|
||||||
const anchor = resultCards[0] as HTMLInputElement | null
|
|
||||||
await displayPreview(anchor)
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
anchor?.click()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) {
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (results?.contains(document.activeElement)) {
|
if (results?.contains(document.activeElement)) {
|
||||||
// If an element in results-container already has focus, focus previous one
|
// If an element in results-container already has focus, focus previous one
|
||||||
const currentResult = document.activeElement as HTMLInputElement | null
|
const currentResult = currentHover
|
||||||
|
? currentHover
|
||||||
|
: (document.activeElement as HTMLInputElement | null)
|
||||||
const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null
|
const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null
|
||||||
currentResult?.classList.remove("focus")
|
currentResult?.classList.remove("focus")
|
||||||
await displayPreview(prevResult)
|
|
||||||
prevResult?.focus()
|
prevResult?.focus()
|
||||||
|
currentHover = prevResult
|
||||||
|
await displayPreview(prevResult)
|
||||||
}
|
}
|
||||||
} else if (e.key === "ArrowDown" || e.key === "Tab") {
|
} else if (e.key === "ArrowDown" || e.key === "Tab") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
// The results should already been focused, so we need to find the next one.
|
// 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.
|
// The activeElement is the search bar, so we need to find the first result and focus it.
|
||||||
if (!results?.contains(document.activeElement)) {
|
if (document.activeElement === searchBar || currentHover !== null) {
|
||||||
const firstResult = resultCards[0] as HTMLInputElement | null
|
const firstResult = currentHover
|
||||||
|
? currentHover
|
||||||
|
: (document.getElementsByClassName("result-card")[0] as HTMLInputElement | null)
|
||||||
const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null
|
const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null
|
||||||
firstResult?.classList.remove("focus")
|
firstResult?.classList.remove("focus")
|
||||||
await displayPreview(secondResult)
|
|
||||||
secondResult?.focus()
|
secondResult?.focus()
|
||||||
|
currentHover = secondResult
|
||||||
|
await displayPreview(secondResult)
|
||||||
} else {
|
} else {
|
||||||
// If an element in results-container already has focus, focus next one
|
// If an element in results-container already has focus, focus next one
|
||||||
const active = document.activeElement as HTMLInputElement | null
|
const active = currentHover
|
||||||
|
? currentHover
|
||||||
|
: (document.activeElement as HTMLInputElement | null)
|
||||||
active?.classList.remove("focus")
|
active?.classList.remove("focus")
|
||||||
const nextResult = active?.nextElementSibling as HTMLInputElement | null
|
const nextResult = active?.nextElementSibling as HTMLInputElement | null
|
||||||
await displayPreview(nextResult)
|
|
||||||
nextResult?.focus()
|
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 formatForDisplay = (term: string, id: number) => {
|
||||||
const slug = idDataMap[id]
|
const slug = idDataMap[id]
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
slug,
|
slug,
|
||||||
title: searchType === "tags" ? data[slug].title : highlight(term, data[slug].title ?? ""),
|
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: highlight(term, data[slug].content ?? "", true),
|
||||||
content:
|
tags: highlightTags(term.substring(1), data[slug].tags),
|
||||||
searchType === "tags"
|
|
||||||
? trimContent(data[slug].content)
|
|
||||||
: highlight(term, data[slug].content ?? "", true),
|
|
||||||
tags: highlightTags(term, data[slug].tags),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightTags(term: string, tags: string[]) {
|
function highlightTags(term: string, tags: string[]) {
|
||||||
if (tags && searchType === "tags") {
|
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 {
|
|
||||||
return []
|
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 {
|
function resolveUrl(slug: FullSlug): URL {
|
||||||
@@ -269,30 +314,26 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resultToHTML = ({ slug, title, content, tags }: Item) => {
|
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 resultContent = enablePreview && window.innerWidth > 600 ? "" : `<p>${content}</p>`
|
|
||||||
|
|
||||||
const itemTile = document.createElement("a")
|
const itemTile = document.createElement("a")
|
||||||
itemTile.classList.add("result-card")
|
itemTile.classList.add("result-card")
|
||||||
Object.assign(itemTile, {
|
itemTile.id = slug
|
||||||
id: slug,
|
itemTile.href = resolveUrl(slug).toString()
|
||||||
href: resolveUrl(slug).toString(),
|
itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}<p class="preview">${content}</p>`
|
||||||
innerHTML: `<h3>${title}</h3>${htmlTags}${resultContent}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
async function onMouseEnter(ev: MouseEvent) {
|
async function onMouseEnter(ev: MouseEvent) {
|
||||||
// When search is active, the first element is in focus, so we need to remove focus if given target is not the first element
|
if (!ev.target) return
|
||||||
const firstEl = document.getElementsByClassName("result-card")[0] as HTMLAnchorElement | null
|
currentHover?.classList.remove("focus")
|
||||||
const target = ev.target as HTMLAnchorElement
|
currentHover?.blur()
|
||||||
if (firstEl !== target) {
|
const target = ev.target as HTMLInputElement
|
||||||
firstEl?.classList.remove("focus")
|
currentHover = target
|
||||||
}
|
currentHover.classList.add("focus")
|
||||||
target.classList.add("focus")
|
|
||||||
await displayPreview(target)
|
await displayPreview(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onMouseLeave(ev: MouseEvent) {
|
async function onMouseLeave(ev: MouseEvent) {
|
||||||
const target = ev.target as HTMLAnchorElement
|
if (!ev.target) return
|
||||||
|
const target = ev.target as HTMLElement
|
||||||
target.classList.remove("focus")
|
target.classList.remove("focus")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,9 +347,12 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
hideSearch()
|
hideSearch()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
] as [keyof HTMLElementEventMap, (this: HTMLElement) => void][]
|
] as const
|
||||||
|
|
||||||
events.forEach(([event, handler]) => itemTile.addEventListener(event, handler))
|
events.forEach(([event, handler]) => {
|
||||||
|
itemTile.addEventListener(event, handler)
|
||||||
|
window.addCleanup(() => itemTile.removeEventListener(event, handler))
|
||||||
|
})
|
||||||
|
|
||||||
return itemTile
|
return itemTile
|
||||||
}
|
}
|
||||||
@@ -318,17 +362,23 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
|
|
||||||
removeAllChildren(results)
|
removeAllChildren(results)
|
||||||
if (finalResults.length === 0) {
|
if (finalResults.length === 0) {
|
||||||
results.innerHTML = `<a class="result-card">
|
results.innerHTML = `<a class="result-card no-match">
|
||||||
<h3>No results.</h3>
|
<h3>No results.</h3>
|
||||||
<p>Try another search term?</p>
|
<p>Try another search term?</p>
|
||||||
</a>`
|
</a>`
|
||||||
} else {
|
} else {
|
||||||
results.append(...finalResults.map(resultToHTML))
|
results.append(...finalResults.map(resultToHTML))
|
||||||
}
|
}
|
||||||
// focus on first result, then also dispatch preview immediately
|
|
||||||
if (results?.firstElementChild) {
|
if (finalResults.length === 0 && preview) {
|
||||||
results?.firstElementChild?.classList.add("focus")
|
// no results, clear previous preview
|
||||||
await displayPreview(results?.firstElementChild as HTMLElement)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,51 +404,42 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function displayPreview(el: HTMLElement | null) {
|
async function displayPreview(el: HTMLElement | null) {
|
||||||
if (!searchLayout || !enablePreview || !el) return
|
if (!searchLayout || !enablePreview || !el || !preview) return
|
||||||
|
|
||||||
const slug = el.id as FullSlug
|
const slug = el.id as FullSlug
|
||||||
el.classList.add("focus")
|
const innerDiv = await fetchContent(slug).then((contents) =>
|
||||||
|
contents.flatMap((el) => [...highlightHTML(currentSearchTerm, el as HTMLElement).children]),
|
||||||
removeAllChildren(preview as HTMLElement)
|
)
|
||||||
const contentDetails = await fetchContent(slug)
|
previewInner = document.createElement("div")
|
||||||
|
|
||||||
const previewInner = document.createElement("div")
|
|
||||||
previewInner.classList.add("preview-inner")
|
previewInner.classList.add("preview-inner")
|
||||||
preview?.appendChild(previewInner)
|
previewInner.append(...innerDiv)
|
||||||
contentDetails?.forEach((elt) => previewInner.appendChild(elt))
|
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"]) {
|
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[]
|
let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[]
|
||||||
|
if (searchType === "tags") {
|
||||||
if (searchLayout) {
|
searchResults = await index.searchAsync({
|
||||||
searchLayout.style.opacity = "1"
|
query: currentSearchTerm.substring(1),
|
||||||
}
|
limit: numSearchResults,
|
||||||
|
index: ["tags"],
|
||||||
if (term.toLowerCase().startsWith("#")) {
|
})
|
||||||
searchType = "tags"
|
} else if (searchType === "basic") {
|
||||||
} else {
|
searchResults = await index.searchAsync({
|
||||||
searchType = "basic"
|
query: currentSearchTerm,
|
||||||
}
|
limit: numSearchResults,
|
||||||
|
index: ["title", "content"],
|
||||||
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"],
|
|
||||||
})) ?? []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getByField = (field: string): number[] => {
|
const getByField = (field: string): number[] => {
|
||||||
@@ -412,50 +453,19 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
...getByField("content"),
|
...getByField("content"),
|
||||||
...getByField("tags"),
|
...getByField("tags"),
|
||||||
])
|
])
|
||||||
const finalResults = [...allIds].map((id) => formatForDisplay(term, id))
|
const finalResults = [...allIds].map((id) => formatForDisplay(currentSearchTerm, id))
|
||||||
await displayResults(finalResults)
|
await displayResults(finalResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevShortcutHandler) {
|
|
||||||
document.removeEventListener("keydown", prevShortcutHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("keydown", shortcutHandler)
|
document.addEventListener("keydown", shortcutHandler)
|
||||||
prevShortcutHandler = shortcutHandler
|
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
|
||||||
searchIcon?.removeEventListener("click", () => showSearch("basic"))
|
|
||||||
searchIcon?.addEventListener("click", () => showSearch("basic"))
|
searchIcon?.addEventListener("click", () => showSearch("basic"))
|
||||||
searchBar?.removeEventListener("input", onType)
|
window.addCleanup(() => searchIcon?.removeEventListener("click", () => showSearch("basic")))
|
||||||
searchBar?.addEventListener("input", onType)
|
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)
|
registerEscapeHandler(container, hideSearch)
|
||||||
|
await fillDocument(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -463,16 +473,20 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
* @param index index to fill
|
* @param index index to fill
|
||||||
* @param data data to fill index with
|
* @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
|
let id = 0
|
||||||
|
const promises: Array<Promise<unknown>> = []
|
||||||
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
||||||
await index.addAsync(id, {
|
promises.push(
|
||||||
id,
|
index.addAsync(id++, {
|
||||||
slug: slug as FullSlug,
|
id,
|
||||||
title: fileData.title,
|
slug: slug as FullSlug,
|
||||||
content: fileData.content,
|
title: fileData.title,
|
||||||
tags: fileData.tags,
|
content: fileData.content,
|
||||||
})
|
tags: fileData.tags,
|
||||||
id++
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ function notifyNav(url: FullSlug) {
|
|||||||
document.dispatchEvent(event)
|
document.dispatchEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cleanupFns: Set<(...args: any[]) => void> = new Set()
|
||||||
|
window.addCleanup = (fn) => cleanupFns.add(fn)
|
||||||
|
|
||||||
let p: DOMParser
|
let p: DOMParser
|
||||||
async function navigate(url: URL, isBack: boolean = false) {
|
async function navigate(url: URL, isBack: boolean = false) {
|
||||||
p = p || new DOMParser()
|
p = p || new DOMParser()
|
||||||
@@ -57,6 +60,10 @@ async function navigate(url: URL, isBack: boolean = false) {
|
|||||||
|
|
||||||
if (!contents) return
|
if (!contents) return
|
||||||
|
|
||||||
|
// cleanup old
|
||||||
|
cleanupFns.forEach((fn) => fn())
|
||||||
|
cleanupFns.clear()
|
||||||
|
|
||||||
const html = p.parseFromString(contents, "text/html")
|
const html = p.parseFromString(contents, "text/html")
|
||||||
normalizeRelativeURLs(html, url)
|
normalizeRelativeURLs(html, url)
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ function setupToc() {
|
|||||||
const content = toc.nextElementSibling as HTMLElement | undefined
|
const content = toc.nextElementSibling as HTMLElement | undefined
|
||||||
if (!content) return
|
if (!content) return
|
||||||
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
|
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
|
||||||
toc.removeEventListener("click", toggleToc)
|
|
||||||
toc.addEventListener("click", toggleToc)
|
toc.addEventListener("click", toggleToc)
|
||||||
|
window.addCleanup(() => toc.removeEventListener("click", toggleToc))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb:
|
|||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
outsideContainer?.removeEventListener("click", click)
|
|
||||||
outsideContainer?.addEventListener("click", click)
|
outsideContainer?.addEventListener("click", click)
|
||||||
document.removeEventListener("keydown", esc)
|
window.addCleanup(() => outsideContainer?.removeEventListener("click", click))
|
||||||
document.addEventListener("keydown", esc)
|
document.addEventListener("keydown", esc)
|
||||||
|
window.addCleanup(() => document.removeEventListener("keydown", esc))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeAllChildren(node: HTMLElement) {
|
export function removeAllChildren(node: HTMLElement) {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@use "../../styles/variables.scss" as *;
|
||||||
|
|
||||||
button#explorer {
|
button#explorer {
|
||||||
all: unset;
|
all: unset;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -85,7 +87,7 @@ svg {
|
|||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
font-family: var(--headerFont);
|
font-family: var(--headerFont);
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
font-weight: $boldWeight;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@@ -110,7 +112,7 @@ svg {
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
font-weight: 600;
|
font-weight: $boldWeight;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -54,15 +54,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > #search-space {
|
& > #search-space {
|
||||||
width: 50%;
|
width: 65%;
|
||||||
margin-top: 12vh;
|
margin-top: 12vh;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|
||||||
@media all and (max-width: $fullPageWidth) {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -87,14 +83,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > #search-layout {
|
& > #search-layout {
|
||||||
display: flex;
|
display: none;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
|
||||||
opacity: 0;
|
|
||||||
border: 1px solid var(--lightgray);
|
border: 1px solid var(--lightgray);
|
||||||
|
|
||||||
|
&.display-results {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: $tabletBreakpoint) {
|
||||||
|
&[data-preview] {
|
||||||
|
& .result-card > p.preview {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
height: calc(75vh - 20em);
|
// vh - #search-space.margin-top
|
||||||
|
height: calc(75vh - 12vh);
|
||||||
background: none;
|
background: none;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
@@ -109,7 +116,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: $mobileBreakpoint) {
|
@media all and (max-width: $tabletBreakpoint) {
|
||||||
display: block;
|
display: block;
|
||||||
& > *:not(#results-container) {
|
& > *:not(#results-container) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@@ -121,25 +128,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& .highlight {
|
||||||
|
background: color-mix(in srgb, var(--tertiary) 60%, transparent);
|
||||||
|
border-radius: 5px;
|
||||||
|
scroll-margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
& > #preview-container {
|
& > #preview-container {
|
||||||
display: block;
|
display: block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: inherit;
|
||||||
|
color: var(--dark);
|
||||||
|
line-height: 1.5em;
|
||||||
|
font-weight: $normalWeight;
|
||||||
|
background: var(--light);
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
& .preview-inner {
|
& .preview-inner {
|
||||||
padding: 1em;
|
margin: 0 auto;
|
||||||
height: 100%;
|
width: min($pageWidth, 100%);
|
||||||
box-sizing: border-box;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-family: inherit;
|
|
||||||
color: var(--dark);
|
|
||||||
line-height: 1.5em;
|
|
||||||
font-weight: 400;
|
|
||||||
background: var(--light);
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow:
|
|
||||||
0 14px 50px rgba(27, 33, 48, 0.12),
|
|
||||||
0 10px 30px rgba(27, 33, 48, 0.16);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,12 +178,6 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
|
|
||||||
& .highlight {
|
|
||||||
color: var(--secondary);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&.focus {
|
&.focus {
|
||||||
background: var(--lightgray);
|
background: var(--lightgray);
|
||||||
@@ -181,41 +187,23 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > ul > li {
|
& > ul.tags {
|
||||||
margin: 0;
|
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin: 0;
|
|
||||||
overflow-wrap: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > ul {
|
|
||||||
list-style: none;
|
|
||||||
display: flex;
|
|
||||||
padding-left: 0;
|
|
||||||
gap: 0.4rem;
|
|
||||||
margin: 0;
|
|
||||||
margin-top: 0.45rem;
|
margin-top: 0.45rem;
|
||||||
box-sizing: border-box;
|
margin-bottom: 0;
|
||||||
overflow: hidden;
|
|
||||||
background-clip: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& > ul > li > p {
|
& > ul > li > p {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--highlight);
|
background-color: var(--highlight);
|
||||||
overflow: hidden;
|
padding: 0.2rem 0.4rem;
|
||||||
background-clip: border-box;
|
margin: 0 0.1rem;
|
||||||
padding: 0.03rem 0.4rem;
|
line-height: 1.4rem;
|
||||||
margin: 0;
|
font-weight: $boldWeight;
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > ul > li > .match-tag {
|
&.match-tag {
|
||||||
color: var(--tertiary);
|
color: var(--tertiary);
|
||||||
font-weight: bold;
|
}
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& > p {
|
& > p {
|
||||||
|
|||||||
@@ -131,9 +131,11 @@ function addGlobalPageResources(
|
|||||||
componentResources.afterDOMLoaded.push(spaRouterScript)
|
componentResources.afterDOMLoaded.push(spaRouterScript)
|
||||||
} else {
|
} else {
|
||||||
componentResources.afterDOMLoaded.push(`
|
componentResources.afterDOMLoaded.push(`
|
||||||
window.spaNavigate = (url, _) => window.location.assign(url)
|
window.spaNavigate = (url, _) => window.location.assign(url)
|
||||||
const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
|
window.addCleanup = () => {}
|
||||||
document.dispatchEvent(event)`)
|
const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
|
||||||
|
document.dispatchEvent(event)
|
||||||
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
let wsUrl = `ws://localhost:${ctx.argv.wsPort}`
|
let wsUrl = `ws://localhost:${ctx.argv.wsPort}`
|
||||||
@@ -147,9 +149,9 @@ function addGlobalPageResources(
|
|||||||
loadTime: "afterDOMReady",
|
loadTime: "afterDOMReady",
|
||||||
contentType: "inline",
|
contentType: "inline",
|
||||||
script: `
|
script: `
|
||||||
const socket = new WebSocket('${wsUrl}')
|
const socket = new WebSocket('${wsUrl}')
|
||||||
socket.addEventListener('message', () => document.location.reload())
|
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 { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path"
|
||||||
import { QuartzEmitterPlugin } from "../types"
|
import { QuartzEmitterPlugin } from "../types"
|
||||||
import { toHtml } from "hast-util-to-html"
|
import { toHtml } from "hast-util-to-html"
|
||||||
import path from "path"
|
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
|
|
||||||
export type ContentIndex = Map<FullSlug, ContentDetails>
|
export type ContentIndex = Map<FullSlug, ContentDetails>
|
||||||
|
|||||||
@@ -383,14 +383,17 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
|
|||||||
const calloutType = canonicalizeCallout(typeString.toLowerCase())
|
const calloutType = canonicalizeCallout(typeString.toLowerCase())
|
||||||
const collapse = collapseChar === "+" || collapseChar === "-"
|
const collapse = collapseChar === "+" || collapseChar === "-"
|
||||||
const defaultState = collapseChar === "-" ? "collapsed" : "expanded"
|
const defaultState = collapseChar === "-" ? "collapsed" : "expanded"
|
||||||
const titleContent =
|
const titleContent = match.input.slice(calloutDirective.length).trim()
|
||||||
match.input.slice(calloutDirective.length).trim() || capitalize(calloutType)
|
const useDefaultTitle = titleContent === "" && restOfTitle.length === 0
|
||||||
const titleNode: Paragraph = {
|
const titleNode: Paragraph = {
|
||||||
type: "paragraph",
|
type: "paragraph",
|
||||||
children:
|
children: [
|
||||||
restOfTitle.length === 0
|
{
|
||||||
? [{ type: "text", value: titleContent + " " }]
|
type: "text",
|
||||||
: restOfTitle,
|
value: useDefaultTitle ? capitalize(calloutType) : titleContent + " ",
|
||||||
|
},
|
||||||
|
...restOfTitle,
|
||||||
|
],
|
||||||
}
|
}
|
||||||
const title = mdastToHtml(titleNode)
|
const title = mdastToHtml(titleNode)
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ section {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background: color-mix(in srgb, var(--tertiary) 75%, transparent);
|
background: color-mix(in srgb, var(--tertiary) 60%, transparent);
|
||||||
color: var(--darkgray);
|
color: var(--darkgray);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ ul,
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-weight: 600;
|
font-weight: $boldWeight;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@use "./variables.scss" as *;
|
||||||
@use "sass:color";
|
@use "sass:color";
|
||||||
|
|
||||||
.callout {
|
.callout {
|
||||||
@@ -156,6 +157,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.callout-title-inner {
|
.callout-title-inner {
|
||||||
font-weight: 700;
|
font-weight: $boldWeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
$pageWidth: 750px;
|
$pageWidth: 750px;
|
||||||
$mobileBreakpoint: 600px;
|
$mobileBreakpoint: 600px;
|
||||||
$tabletBreakpoint: 1200px;
|
$tabletBreakpoint: 1000px;
|
||||||
$sidePanelWidth: 380px;
|
$sidePanelWidth: 380px;
|
||||||
$topSpacing: 6rem;
|
$topSpacing: 6rem;
|
||||||
$fullPageWidth: $pageWidth + 2 * $sidePanelWidth;
|
$fullPageWidth: $pageWidth + 2 * $sidePanelWidth;
|
||||||
|
$boldWeight: 700;
|
||||||
|
$normalWeight: 400;
|
||||||
|
|||||||
Reference in New Issue
Block a user