Compare commits

...

82 Commits

Author SHA1 Message Date
Emile Bangma
7d7e334976 feat: responsive design grid (#1354)
* Responsive design grid

* Addressed PR feedback

* Bump Quartz version 4.3.1 => 4.4.0

* Moved page-header into center

* Updated docs with new layouts

* Sync updated version number with package-lock

* Table of Content scrollbar auto

* Reset node_modules

* Updated layout images

* Fixed tablet layout

* Finilazed layout images
2024-09-23 13:40:12 -07:00
Jacky Zhao
9cefcd0dd1 Revert "perf: eagerly compute explorer nodes to avoid re-render in memoized value"
This reverts commit 16a9caa555.
2024-09-20 15:48:14 -07:00
Gustavo de Paula
4aaeb768d8 update cabin url (#1428) 2024-09-20 09:23:19 -07:00
dependabot[bot]
743ef712d5 chore(deps): bump preact-render-to-string from 6.5.10 to 6.5.11 (#1417)
Bumps [preact-render-to-string](https://github.com/preactjs/preact-render-to-string) from 6.5.10 to 6.5.11.
- [Release notes](https://github.com/preactjs/preact-render-to-string/releases)
- [Changelog](https://github.com/preactjs/preact-render-to-string/blob/main/CHANGELOG.md)
- [Commits](https://github.com/preactjs/preact-render-to-string/compare/v6.5.10...v6.5.11)

---
updated-dependencies:
- dependency-name: preact-render-to-string
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 22:24:03 -04:00
dependabot[bot]
9a6e4e2f80 chore(deps-dev): bump typescript from 5.5.4 to 5.6.2 (#1418)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.5.4 to 5.6.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.5.4...v5.6.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 19:56:14 -04:00
dependabot[bot]
14cb50de9b chore(deps): bump preact from 10.23.2 to 10.24.0 (#1419)
Bumps [preact](https://github.com/preactjs/preact) from 10.23.2 to 10.24.0.
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.23.2...10.24.0)

---
updated-dependencies:
- dependency-name: preact
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 19:56:00 -04:00
dependabot[bot]
dad0ae4e3f chore(deps-dev): bump @types/node from 22.5.4 to 22.5.5 (#1420)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.5.4 to 22.5.5.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 19:55:42 -04:00
dependabot[bot]
9c060f3cf2 chore(deps): bump vfile from 6.0.2 to 6.0.3 (#1421)
Bumps [vfile](https://github.com/vfile/vfile) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/vfile/vfile/releases)
- [Changelog](https://github.com/vfile/vfile/blob/main/changelog.md)
- [Commits](https://github.com/vfile/vfile/compare/6.0.2...6.0.3)

---
updated-dependencies:
- dependency-name: vfile
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 19:55:23 -04:00
Jacky Zhao
16a9caa555 perf: eagerly compute explorer nodes to avoid re-render in memoized value 2024-09-15 18:05:17 -07:00
Alexander Weichart
b1c60b8833 Add alexanderweichart.de to showcase.md (#1378)
Added my own website ^^

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2024-09-15 17:03:22 -07:00
Jacky Zhao
50a78bafa5 docs: clarify prod hosting 2024-09-15 16:47:11 -07:00
Jacky Zhao
6ea359e55e perf: have more than 1ms granularity for rebuild detection 2024-09-15 16:42:07 -07:00
Emile Bangma
cd3bb25626 fix: account for subtags in numerical tags (closes #1408) (#1410) 2024-09-15 11:25:31 -04:00
bfahrenfort
eb9bbd1666 fix: constrain link icon size (#1409) 2024-09-14 10:10:01 -07:00
dependabot[bot]
c36551310d chore(deps): bump preact-render-to-string from 6.5.9 to 6.5.10 (#1390)
Bumps [preact-render-to-string](https://github.com/preactjs/preact-render-to-string) from 6.5.9 to 6.5.10.
- [Release notes](https://github.com/preactjs/preact-render-to-string/releases)
- [Changelog](https://github.com/preactjs/preact-render-to-string/blob/main/CHANGELOG.md)
- [Commits](https://github.com/preactjs/preact-render-to-string/compare/v6.5.9...v6.5.10)

---
updated-dependencies:
- dependency-name: preact-render-to-string
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-11 14:46:22 -07:00
Jacky Zhao
6215dd5565 docs: fix roam research docs 2024-09-11 14:45:04 -07:00
dependabot[bot]
c89c76b40a chore(deps-dev): bump tsx from 4.18.0 to 4.19.0 (#1391)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.18.0 to 4.19.0.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.18.0...v4.19.0)

---
updated-dependencies:
- dependency-name: tsx
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-11 03:30:44 -04:00
dependabot[bot]
a145e320d0 chore(deps): bump hast-util-to-html from 9.0.1 to 9.0.2 (#1393)
Bumps [hast-util-to-html](https://github.com/syntax-tree/hast-util-to-html) from 9.0.1 to 9.0.2.
- [Release notes](https://github.com/syntax-tree/hast-util-to-html/releases)
- [Commits](https://github.com/syntax-tree/hast-util-to-html/compare/9.0.1...9.0.2)

---
updated-dependencies:
- dependency-name: hast-util-to-html
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-11 03:30:28 -04:00
dependabot[bot]
082cbb74c3 chore(deps-dev): bump @types/node from 22.5.0 to 22.5.4 (#1403)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.5.0 to 22.5.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-11 03:29:51 -04:00
dependabot[bot]
93b2481261 chore(deps): bump pixi.js from 8.3.3 to 8.3.4 (#1394)
Bumps [pixi.js](https://github.com/pixijs/pixijs) from 8.3.3 to 8.3.4.
- [Release notes](https://github.com/pixijs/pixijs/releases)
- [Commits](https://github.com/pixijs/pixijs/compare/v8.3.3...v8.3.4)

---
updated-dependencies:
- dependency-name: pixi.js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-11 03:29:36 -04:00
Matt Vogel
d2414b3903 feat(markdown): Roam Research flavour (#985)
* feat: Roam Research flavor markdown

* docs: Roam Research transformer

* use markdownPlugins

* fix roam matching

* cleanup: Roam Plugin

---------

Co-authored-by: Matt Vogel <>
2024-09-11 03:29:07 -04:00
MarcRez33
01fc26d2c0 fix: Reload graph after a theme change (closes #1380) (#1383)
* FIX: Reload graph after a theme change

* FIX: Reload graph after a theme change - comment updated

* FIX: Reload graph after a theme change - comment updated

* FIX: Reload graph after a theme change

* fix: Reload graph after a theme change
2024-08-31 12:25:30 -07:00
bfahrenfort
84a9be65ce Add Projects & Privacy to showcase.md (#1381) 2024-08-31 11:43:49 -07:00
Jacky Zhao
6715079a89 fix: only apply z-index on left sidebar 2024-08-28 12:50:25 -07:00
dependabot[bot]
d613a3d2f2 chore(deps): bump unified from 11.0.4 to 11.0.5 (#1373)
Bumps [unified](https://github.com/unifiedjs/unified) from 11.0.4 to 11.0.5.
- [Release notes](https://github.com/unifiedjs/unified/releases)
- [Changelog](https://github.com/unifiedjs/unified/blob/main/changelog.md)
- [Commits](https://github.com/unifiedjs/unified/compare/11.0.4...11.0.5)

---
updated-dependencies:
- dependency-name: unified
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 14:13:09 -07:00
dependabot[bot]
9b75faafea chore(deps-dev): bump @types/yargs from 17.0.32 to 17.0.33 (#1374)
Bumps [@types/yargs](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/yargs) from 17.0.32 to 17.0.33.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/yargs)

---
updated-dependencies:
- dependency-name: "@types/yargs"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 14:13:00 -07:00
dependabot[bot]
5a26b582ed chore(deps-dev): bump @types/node from 22.1.0 to 22.5.0 (#1375)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.1.0 to 22.5.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 14:12:51 -07:00
dependabot[bot]
4e2aea8a5a chore(deps): bump @napi-rs/simple-git from 0.1.17 to 0.1.19 (#1376)
Bumps [@napi-rs/simple-git](https://github.com/Brooooooklyn/simple-git) from 0.1.17 to 0.1.19.
- [Release notes](https://github.com/Brooooooklyn/simple-git/releases)
- [Commits](https://github.com/Brooooooklyn/simple-git/compare/v0.1.17...v0.1.19)

---
updated-dependencies:
- dependency-name: "@napi-rs/simple-git"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 14:12:45 -07:00
dependabot[bot]
40f039983c chore(deps-dev): bump tsx from 4.17.0 to 4.18.0 (#1377)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.17.0 to 4.18.0.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.17.0...v4.18.0)

---
updated-dependencies:
- dependency-name: tsx
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 14:12:35 -07:00
Iker Larrea
3b988aec61 fix: Popover z-index issue on left sidebar (#1230) 2024-08-25 19:33:34 -07:00
Aaron Pham
bca74623a3 perf(graph): canvas implementation (#1328)
* perf(graph): initial canvas layout

include nodes and links drawn

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* fix(graph): update persistent for nodeGfx

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* chore(graph): add canvas element to avoid rerendering glitch

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* fix(spa): only render graph once in global

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* fix(graph): change svg as button

render global graph on toggle

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* fix(graph): fix anchor position and zIndex behaviour

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* chore(graph): increase linkDistance

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* refactor

* fmt

* pkg

---------

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2024-08-25 00:33:14 -07:00
Andrew
4c9e860150 a11y(darkmode): use a button for the theme toggle (#1335)
* Use a `<button>` for theme toggle

* docs: Adds back Xinyang's cs garden to showcase (#1323)

adding back my garden which was deleted from the cleanup showcase

* feat(toc,explorer): add accessibility for toggle (#1327)

* Restore focus highlight on explorer toggle button.

Remove `unset: all` declaration causing `outline`
property to be unset. This allows the default
browser focus highlight to be shown.

* Fix semantics of expandable sections (explorer, toc).

This adds the appropriate aria attributes for the [disclosure pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-image-description/#javascriptandcsssourcecode) and uses `visibility: hidden` to remove the hidden elements from the focus order without disrupting the animations. Further work is needed on the tree view nodes.

* Run prettier for SCSS files.

* feat: custom global latex macros (closes #1325)

* chore: ts fixes

* docs: recommend at least node 20 in gh

* fix: unmemoize explorer on rebuild (closes #1077)

* fix: pass buildId to worker

* Fix theme button DOM hierarchy and styles

* Restore functionality of theme button

* `aria-label` on theme svgs so their accessible labels are included in button content

---------

Co-authored-by: Xinyang Yu <47915643+xy-241@users.noreply.github.com>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2024-08-24 13:33:30 -07:00
Danny van Tol
46b63b68bf fix: Link visibility in exampanded TOC while collapseByDefault is true (#1371) 2024-08-24 12:56:17 -07:00
Andrew
c4cd84dcc8 fix: Add a wrapper element to dates in PageList. (#1345)
* Add a wrapper span to dates in PageList.

This means there is a placeholder when date is not specified, so the values in grid-template-columns always line up correctly.

* Use a <div> instead -- better practice to stick to block elements.
2024-08-24 10:13:55 -07:00
Aaron Pham
2be9c096a1 chore(features): remove implemented (#1350) 2024-08-24 10:13:14 -07:00
Tony Jackson
f913332862 docs: Update Gitlab CI runner tag to use Gitlab runner (#1365)
Co-authored-by: Tony Jackson <tony.aaj.jackson@protonmail.com>
2024-08-23 18:55:03 -07:00
Elijah Kennedy
647c125525 docs: added permalink description to supported frontmatter doc (#1369) 2024-08-23 18:35:52 -07:00
Eledah
3b74453fe6 docs: RTL showcase (#1362) 2024-08-20 09:28:52 -07:00
dependabot[bot]
6b499ed90c chore(deps): bump @floating-ui/dom from 1.6.8 to 1.6.10 (#1358)
Bumps [@floating-ui/dom](https://github.com/floating-ui/floating-ui/tree/HEAD/packages/dom) from 1.6.8 to 1.6.10.
- [Release notes](https://github.com/floating-ui/floating-ui/releases)
- [Changelog](https://github.com/floating-ui/floating-ui/blob/master/packages/dom/CHANGELOG.md)
- [Commits](https://github.com/floating-ui/floating-ui/commits/@floating-ui/dom@1.6.10/packages/dom)

---
updated-dependencies:
- dependency-name: "@floating-ui/dom"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 14:18:00 -07:00
dependabot[bot]
437d65c847 chore(deps): bump lightningcss from 1.25.1 to 1.26.0 (#1359)
Bumps [lightningcss](https://github.com/parcel-bundler/lightningcss) from 1.25.1 to 1.26.0.
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/compare/v1.25.1...v1.26.0)

---
updated-dependencies:
- dependency-name: lightningcss
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 14:15:43 -07:00
dependabot[bot]
7c5709b660 chore(deps): bump preact-render-to-string from 6.5.7 to 6.5.9 (#1360)
Bumps [preact-render-to-string](https://github.com/preactjs/preact-render-to-string) from 6.5.7 to 6.5.9.
- [Release notes](https://github.com/preactjs/preact-render-to-string/releases)
- [Changelog](https://github.com/preactjs/preact-render-to-string/blob/main/CHANGELOG.md)
- [Commits](https://github.com/preactjs/preact-render-to-string/compare/v6.5.7...v6.5.9)

---
updated-dependencies:
- dependency-name: preact-render-to-string
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 14:15:34 -07:00
dependabot[bot]
21e1921822 chore(deps): bump @napi-rs/simple-git from 0.1.16 to 0.1.17 (#1357)
Bumps [@napi-rs/simple-git](https://github.com/Brooooooklyn/simple-git) from 0.1.16 to 0.1.17.
- [Release notes](https://github.com/Brooooooklyn/simple-git/releases)
- [Commits](https://github.com/Brooooooklyn/simple-git/compare/v0.1.16...v0.1.17)

---
updated-dependencies:
- dependency-name: "@napi-rs/simple-git"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 14:14:45 -07:00
dependabot[bot]
e47c29d2fd chore(deps): bump rehype-katex from 7.0.0 to 7.0.1 (#1356)
Bumps [rehype-katex](https://github.com/remarkjs/remark-math) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/remarkjs/remark-math/releases)
- [Commits](https://github.com/remarkjs/remark-math/compare/rehype-katex@7.0.0...rehype-katex@7.0.1)

---
updated-dependencies:
- dependency-name: rehype-katex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 14:14:29 -07:00
dependabot[bot]
a5f2f874f7 chore(deps-dev): bump tsx from 4.16.2 to 4.17.0 (#1340)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.16.2 to 4.17.0.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.16.2...v4.17.0)

---
updated-dependencies:
- dependency-name: tsx
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-15 05:07:05 -04:00
dependabot[bot]
eb23cbe8da chore(deps): bump rehype-citation from 2.0.0 to 2.1.1 (#1341)
Bumps [rehype-citation](https://github.com/timlrx/rehype-citation) from 2.0.0 to 2.1.1.
- [Release notes](https://github.com/timlrx/rehype-citation/releases)
- [Commits](https://github.com/timlrx/rehype-citation/compare/v2.0.0...v2.1.1)

---
updated-dependencies:
- dependency-name: rehype-citation
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-15 04:25:07 -04:00
dependabot[bot]
cb68069d45 chore(deps-dev): bump typescript from 5.5.3 to 5.5.4 (#1343)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 5.5.3 to 5.5.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v5.5.3...v5.5.4)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-15 04:24:48 -04:00
Andrew
d27c292736 feat: Mark the external link icon as aria-hidden (#1346) 2024-08-12 20:06:19 -07:00
dependabot[bot]
0ee103a514 chore(deps): bump shiki from 1.10.3 to 1.12.1 (#1344)
Bumps [shiki](https://github.com/shikijs/shiki/tree/HEAD/packages/shiki) from 1.10.3 to 1.12.1.
- [Release notes](https://github.com/shikijs/shiki/releases)
- [Changelog](https://github.com/shikijs/shiki/blob/main/CHANGELOG.md)
- [Commits](https://github.com/shikijs/shiki/commits/v1.12.1/packages/shiki)

---
updated-dependencies:
- dependency-name: shiki
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-12 13:38:15 -07:00
dependabot[bot]
c5f0b69a52 chore(deps): bump preact from 10.22.1 to 10.23.2 (#1342)
Bumps [preact](https://github.com/preactjs/preact) from 10.22.1 to 10.23.2.
- [Release notes](https://github.com/preactjs/preact/releases)
- [Commits](https://github.com/preactjs/preact/compare/10.22.1...10.23.2)

---
updated-dependencies:
- dependency-name: preact
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-12 13:37:54 -07:00
Karthi Fair Hawn
323167a001 docs(hosting): missing key with: (#1334) 2024-08-10 08:32:51 -07:00
Andrew
3b5ed813f5 feat(search): keyboard-accessible search button (#1331)
* Use a `<button>` for search

* Fix search button styles to match preexisting styling

* Remove additional native button properties.

* Invoke search button on click or keyboard.

* Reorganize search button DOM hierarchy

* Restore focus to the search button when exiting the search overlay

* Run prettier on Search.tsx
2024-08-09 18:46:50 -07:00
Jacky Zhao
195fc5134c fix: pass buildId to worker 2024-08-08 19:33:27 -07:00
Jacky Zhao
e89c395f7c fix: unmemoize explorer on rebuild (closes #1077) 2024-08-08 19:17:20 -07:00
Jacky Zhao
2db735a150 docs: recommend at least node 20 in gh 2024-08-08 18:38:17 -07:00
Jacky Zhao
39eebca3cf chore: ts fixes 2024-08-08 18:28:13 -07:00
Jacky Zhao
9acaa1c8ac feat: custom global latex macros (closes #1325) 2024-08-08 18:19:45 -07:00
Andrew
27a41abb62 feat(toc,explorer): add accessibility for toggle (#1327)
* Restore focus highlight on explorer toggle button.

Remove `unset: all` declaration causing `outline`
property to be unset. This allows the default
browser focus highlight to be shown.

* Fix semantics of expandable sections (explorer, toc).

This adds the appropriate aria attributes for the [disclosure pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-image-description/#javascriptandcsssourcecode) and uses `visibility: hidden` to remove the hidden elements from the focus order without disrupting the animations. Further work is needed on the tree view nodes.

* Run prettier for SCSS files.
2024-08-08 18:07:47 -07:00
Xinyang Yu
12904ab796 docs: Adds back Xinyang's cs garden to showcase (#1323)
adding back my garden which was deleted from the cleanup showcase
2024-08-08 18:00:20 -07:00
Jacky Zhao
4bbcc0c50a pkg: minor bump for breaking nodejs bump :) 2024-08-05 19:33:23 -07:00
Jacky Zhao
3938904cd0 fix: embed pdf aspect ratio (closes #1310) 2024-08-05 19:31:54 -07:00
Jacky Zhao
407fad384c fix: only one h1 on a page (closes #1269) 2024-08-05 19:18:48 -07:00
Jacky Zhao
ca3943b500 fix: responsive youtube embed (closes #1167) 2024-08-05 19:14:14 -07:00
dependabot[bot]
6c4ed249ba chore(deps): bump rimraf from 5.0.7 to 6.0.1 (#1277)
Bumps [rimraf](https://github.com/isaacs/rimraf) from 5.0.7 to 6.0.1.
- [Changelog](https://github.com/isaacs/rimraf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/rimraf/compare/v5.0.7...v6.0.1)

---
updated-dependencies:
- dependency-name: rimraf
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 18:54:10 -07:00
Jacky Zhao
563ab4aaaf docs: update node version in hosting docs 2024-08-05 18:49:20 -07:00
Jacky Zhao
1c2d542138 build: add .node-version 2024-08-05 18:48:29 -07:00
Pelayo Arbués
e864740df7 docs: Adds back Pelayo Arbues blog to showcase (#1314)
Co-authored-by: Pelayo Arbues <pgarbues@idealista.com>
2024-08-05 18:44:34 -07:00
Ellie Huxtable
efed544df1 docs: Add "Ellie's Notes" to the showcase (#1315)
Thank you for Quartz! I really love using it. Just adding my notes to the showcase 😊
2024-08-05 18:43:57 -07:00
Jacky Zhao
3d156b8497 deps(dev): bump nodejs in ci 2024-08-05 18:43:05 -07:00
Jacky Zhao
38361aaf48 deps: change min required nodejs to v20 (breaking) 2024-08-05 18:41:46 -07:00
dependabot[bot]
f3e07fd51c chore(deps-dev): bump prettier from 3.3.2 to 3.3.3 (#1293)
* chore(deps-dev): bump prettier from 3.3.2 to 3.3.3

Bumps [prettier](https://github.com/prettier/prettier) from 3.3.2 to 3.3.3.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.3.2...3.3.3)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* make prettier happy

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2024-08-05 18:38:07 -07:00
dependabot[bot]
d79911fa79 chore(deps): bump workerpool from 9.1.2 to 9.1.3 (#1318)
Bumps [workerpool](https://github.com/josdejong/workerpool) from 9.1.2 to 9.1.3.
- [Changelog](https://github.com/josdejong/workerpool/blob/master/HISTORY.md)
- [Commits](https://github.com/josdejong/workerpool/compare/v9.1.2...v9.1.3)

---
updated-dependencies:
- dependency-name: workerpool
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 18:46:00 -04:00
dependabot[bot]
963c7c8654 chore(deps-dev): bump @types/node from 20.14.11 to 22.1.0 (#1319)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.14.11 to 22.1.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 16:49:37 -04:00
dependabot[bot]
3728929ee6 chore(deps): bump preact-render-to-string from 6.5.5 to 6.5.7 (#1317)
Bumps [preact-render-to-string](https://github.com/preactjs/preact-render-to-string) from 6.5.5 to 6.5.7.
- [Release notes](https://github.com/preactjs/preact-render-to-string/releases)
- [Changelog](https://github.com/preactjs/preact-render-to-string/blob/main/CHANGELOG.md)
- [Commits](https://github.com/preactjs/preact-render-to-string/compare/v6.5.5...v6.5.7)

---
updated-dependencies:
- dependency-name: preact-render-to-string
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 16:49:19 -04:00
Aaron Pham
1224c7d32f refactor(comments): move script to files (#1308)
* refactor(comments): move script to files

for LSP, treesitter, and the whole galore.

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* fix(type): support removeEventListener with CustomEventMap

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* fix: parse bool to string first

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* chore: address comments and test on branch

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

* revert: remove comments section from main quartz pages

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>

---------

Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
2024-08-05 15:17:11 -04:00
dependabot[bot]
bf1c9d1791 chore(deps): bump globby from 14.0.1 to 14.0.2 (#1301)
Bumps [globby](https://github.com/sindresorhus/globby) from 14.0.1 to 14.0.2.
- [Release notes](https://github.com/sindresorhus/globby/releases)
- [Commits](https://github.com/sindresorhus/globby/compare/v14.0.1...v14.0.2)

---
updated-dependencies:
- dependency-name: globby
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-02 14:48:54 -04:00
Jacky Zhao
6264f5685c fix: comments on spa should work (closes #1296) (#1298)
* fix comments on spa

* fix giscus
2024-07-30 01:13:13 -07:00
Jacky Zhao
e1a9661be7 docs: cleanup showcase 2024-07-29 16:33:50 -07:00
dependabot[bot]
bc95332fce chore(deps-dev): bump @types/ws from 8.5.11 to 8.5.12 (#1300)
Bumps [@types/ws](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/ws) from 8.5.11 to 8.5.12.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/ws)

---
updated-dependencies:
- dependency-name: "@types/ws"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-29 13:14:34 -07:00
dependabot[bot]
740172acb6 chore(deps): bump vfile from 6.0.1 to 6.0.2 (#1299)
Bumps [vfile](https://github.com/vfile/vfile) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/vfile/vfile/releases)
- [Changelog](https://github.com/vfile/vfile/blob/main/changelog.md)
- [Commits](https://github.com/vfile/vfile/compare/6.0.1...6.0.2)

---
updated-dependencies:
- dependency-name: vfile
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-29 13:14:25 -07:00
dependabot[bot]
34fde07cf8 chore(deps-dev): bump tsx from 4.16.0 to 4.16.2 (#1292)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.16.0 to 4.16.2.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.16.0...v4.16.2)

---
updated-dependencies:
- dependency-name: tsx
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-22 15:30:44 -07:00
dependabot[bot]
e688eeeaff chore(deps-dev): bump @types/node from 20.12.5 to 20.14.11 (#1291)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.12.5 to 20.14.11.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-22 15:30:35 -07:00
dependabot[bot]
5749fbbd75 chore(deps): bump @floating-ui/dom from 1.6.5 to 1.6.8 (#1290)
Bumps [@floating-ui/dom](https://github.com/floating-ui/floating-ui/tree/HEAD/packages/dom) from 1.6.5 to 1.6.8.
- [Release notes](https://github.com/floating-ui/floating-ui/releases)
- [Changelog](https://github.com/floating-ui/floating-ui/blob/master/packages/dom/CHANGELOG.md)
- [Commits](https://github.com/floating-ui/floating-ui/commits/@floating-ui/dom@1.6.8/packages/dom)

---
updated-dependencies:
- dependency-name: "@floating-ui/dom"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-22 15:30:28 -07:00
64 changed files with 1862 additions and 914 deletions

View File

@@ -26,7 +26,7 @@ jobs:
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20
- name: Cache dependencies - name: Cache dependencies
uses: actions/cache@v4 uses: actions/cache@v4
@@ -59,7 +59,7 @@ jobs:
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20
- name: Get package version - name: Get package version
run: node -p -e '`PACKAGE_VERSION=${require("./package.json").version}`' >> $GITHUB_ENV run: node -p -e '`PACKAGE_VERSION=${require("./package.json").version}`' >> $GITHUB_ENV
- name: Create release tag - name: Create release tag

1
.node-version Normal file
View File

@@ -0,0 +1 @@
v20.9.0

View File

@@ -29,6 +29,7 @@ Some common frontmatter fields that are natively supported by Quartz:
- `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title. - `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title.
- `description`: Description of the page used for link previews. - `description`: Description of the page used for link previews.
- `permalink`: A custom URL for the page that will remain constant even if the path to the file changes.
- `aliases`: Other names for this note. This is a list of strings. - `aliases`: Other names for this note. This is a list of strings.
- `tags`: Tags for this note. - `tags`: Tags for this note.
- `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz. - `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz.

View File

@@ -21,3 +21,7 @@ This will start a local web server to run your Quartz on your computer. Open a w
> - `--serve`: run a local hot-reloading server to preview your Quartz > - `--serve`: run a local hot-reloading server to preview your Quartz
> - `--port`: what port to run the local preview server on > - `--port`: what port to run the local preview server on
> - `--concurrency`: how many threads to use to parse notes > - `--concurrency`: how many threads to use to parse notes
> [!warning] Not to be used for production
> Serve mode is intended for local previews only.
> For production workloads, see the page on [[hosting]].

View File

@@ -0,0 +1,28 @@
---
title: "Roam Research Compatibility"
tags:
- feature/transformer
---
[Roam Research](https://roamresearch.com) is a note-taking tool that organizes your knowledge graph in a unique and interconnected way.
Quartz supports transforming the special Markdown syntax from Roam Research (like `{{[[components]]}}` and other formatting) into
regular Markdown via the [[RoamFlavoredMarkdown]] plugin.
```typescript title="quartz.config.ts"
plugins: {
transformers: [
// ...
Plugin.RoamFlavoredMarkdown(),
Plugin.ObsidianFlavoredMarkdown(),
// ...
],
},
```
> [!warning]
> As seen above placement of `Plugin.RoamFlavoredMarkdown()` within `quartz.config.ts` is very important. It must come before `Plugin.ObsidianFlavoredMarkdown()`.
## Customization
This functionality is provided by the [[RoamFlavoredMarkdown]] plugin. See the plugin page for customization options.

View File

@@ -6,7 +6,6 @@ draft: true
- static dead link detection - static dead link detection
- cursor chat extension - cursor chat extension
- https://giscus.app/ extension
- sidenotes? https://github.com/capnfabs/paperesque - sidenotes? https://github.com/capnfabs/paperesque
- direct match in search using double quotes - direct match in search using double quotes
- https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI - https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI

View File

@@ -61,6 +61,8 @@ jobs:
with: with:
fetch-depth: 0 # Fetch all history for git info fetch-depth: 0 # Fetch all history for git info
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with:
node-version: 22
- name: Install Dependencies - name: Install Dependencies
run: npm ci run: npm ci
- name: Build Quartz - name: Build Quartz
@@ -187,7 +189,7 @@ stages:
- build - build
- deploy - deploy
image: node:18 image: node:20
cache: # Cache modules in between jobs cache: # Cache modules in between jobs
key: $CI_COMMIT_REF_SLUG key: $CI_COMMIT_REF_SLUG
paths: paths:
@@ -206,7 +208,7 @@ build:
paths: paths:
- public - public
tags: tags:
- docker - gitlab-org-docker
pages: pages:
stage: deploy stage: deploy

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -6,7 +6,7 @@ Quartz is a fast, batteries-included static-site generator that transforms Markd
## 🪴 Get Started ## 🪴 Get Started
Quartz requires **at least [Node](https://nodejs.org/) v18.14** and `npm` v9.3.1 to function correctly. Ensure you have this installed on your machine before continuing. Quartz requires **at least [Node](https://nodejs.org/) v20** and `npm` v9.3.1 to function correctly. Ensure you have this installed on your machine before continuing.
Then, in your terminal of choice, enter the following commands line by line: Then, in your terminal of choice, enter the following commands line by line:

View File

@@ -13,15 +13,19 @@ export interface FullPageLayout {
beforeBody: QuartzComponent[] // laid out vertically beforeBody: QuartzComponent[] // laid out vertically
pageBody: QuartzComponent // single component pageBody: QuartzComponent // single component
afterBody: QuartzComponent[] // laid out vertically afterBody: QuartzComponent[] // laid out vertically
left: QuartzComponent[] // vertical on desktop, horizontal on mobile left: QuartzComponent[] // vertical on desktop and tablet, horizontal on mobile
right: QuartzComponent[] // vertical on desktop, horizontal on mobile right: QuartzComponent[] // vertical on desktop, horizontal on tablet and mobile
footer: QuartzComponent // single component footer: QuartzComponent // single component
} }
``` ```
These correspond to following parts of the page: These correspond to following parts of the page:
![[quartz layout.png|800]] | Layout | Preview |
| ------------------------------- | ----------------------------------- |
| Desktop (width > 1200px) | ![[quartz-layout-desktop.png\|800]] |
| Tablet (800px < width < 1200px) | ![[quartz-layout-tablet.png\|800]] |
| Mobile (width < 800px) | ![[quartz-layout-mobile.png\|800]] |
> [!note] > [!note]
> There are two additional layout fields that are _not_ shown in the above diagram. > There are two additional layout fields that are _not_ shown in the above diagram.
@@ -33,6 +37,23 @@ Quartz **components**, like plugins, can take in additional properties as config
See [a list of all the components](component.md) for all available components along with their configuration options. You can also checkout the guide on [[creating components]] if you're interested in further customizing the behaviour of Quartz. See [a list of all the components](component.md) for all available components along with their configuration options. You can also checkout the guide on [[creating components]] if you're interested in further customizing the behaviour of Quartz.
### Layout breakpoints
Quartz has different layouts depending on the width the screen viewing the website.
The breakpoints for layouts can be configured in `variables.scss`.
- `mobile`: screen width below this size will use mobile layout.
- `desktop`: screen width above this size will use desktop layout.
- Screen width between `mobile` and `desktop` width will use the tablet layout.
```scss
$breakpoints: (
mobile: 800px,
desktop: 1200px,
);
```
### Style ### Style
Most meaningful style changes like colour scheme and font can be done simply through the [[configuration#General Configuration|general configuration]] options. However, if you'd like to make more involved style changes, you can do this by writing your own styles. Quartz 4, like Quartz 3, uses [Sass](https://sass-lang.com/guide/) for styling. Most meaningful style changes like colour scheme and font can be done simply through the [[configuration#General Configuration|general configuration]] options. However, if you'd like to make more involved style changes, you can do this by writing your own styles. Quartz 4, like Quartz 3, uses [Sass](https://sass-lang.com/guide/) for styling.

View File

@@ -12,6 +12,7 @@ This plugin adds LaTeX support to Quartz. See [[features/Latex|Latex]] for more
This plugin accepts the following configuration options: This plugin accepts the following configuration options:
- `renderEngine`: the engine to use to render LaTeX equations. Can be `"katex"` for [KaTeX](https://katex.org/) or `"mathjax"` for [MathJax](https://www.mathjax.org/) [SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html). Defaults to KaTeX. - `renderEngine`: the engine to use to render LaTeX equations. Can be `"katex"` for [KaTeX](https://katex.org/) or `"mathjax"` for [MathJax](https://www.mathjax.org/) [SVG rendering](https://docs.mathjax.org/en/latest/output/svg.html). Defaults to KaTeX.
- `customMacros`: custom macros for all LaTeX blocks. It takes the form of a key-value pair where the key is a new command name and the value is the expansion of the macro. For example: `{"\\R": "\\mathbb{R}"}`
## API ## API

View File

@@ -0,0 +1,26 @@
---
title: RoamFlavoredMarkdown
tags:
- plugin/transformer
---
This plugin provides support for [Roam Research](https://roamresearch.com) compatibility. See [[Roam Research Compatibility]] for more information.
> [!note]
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
This plugin accepts the following configuration options:
- `orComponent`: If `true` (default), converts Roam `{{ or:ONE|TWO|THREE }}` shortcodes into HTML Dropdown options.
- `TODOComponent`: If `true` (default), converts Roam `{{[[TODO]]}}` shortcodes into HTML check boxes.
- `DONEComponent`: If `true` (default), converts Roam `{{[[DONE]]}}` shortcodes into checked HTML check boxes.
- `videoComponent`: If `true` (default), converts Roam `{{[[video]]:URL}}` shortcodes into embeded HTML video.
- `audioComponent`: If `true` (default), converts Roam `{{[[audio]]:URL}}` shortcodes into embeded HTML audio.
- `pdfComponent`: If `true` (default), converts Roam `{{[[pdf]]:URL}}` shortcodes into embeded HTML PDF viewer.
- `blockquoteComponent`: If `true` (default), converts Roam `{{[[>]]}}` shortcodes into Quartz blockquotes.
## API
- Category: Transformer
- Function name: `Plugin.RoamFlavoredMarkdown()`.
- Source: [`quartz/plugins/transformers/roam.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/roam.ts).

View File

@@ -7,27 +7,26 @@ Want to see what Quartz can do? Here are some cool community gardens:
- [Quartz Documentation (this site!)](https://quartz.jzhao.xyz/) - [Quartz Documentation (this site!)](https://quartz.jzhao.xyz/)
- [Jacky Zhao's Garden](https://jzhao.xyz/) - [Jacky Zhao's Garden](https://jzhao.xyz/)
- [Socratica Toolbox](https://toolbox.socratica.info/) - [Socratica Toolbox](https://toolbox.socratica.info/)
- [oldwinter の数字花园](https://garden.oldwinter.top/) - [Morrowind Modding Wiki](https://morrowind-modding.github.io/)
- [Aaron Pham's Garden](https://aarnphm.xyz/) - [Aaron Pham's Garden](https://aarnphm.xyz/)
- [The Quantum Garden](https://quantumgardener.blog/) - [Pelayo Arbues' Notes](https://pelayoarbues.com/)
- [Abhijeet's Math Wiki](https://abhmul.github.io/quartz/Math-Wiki/) - [Stanford CME 302 Numerical Linear Algebra](https://ericdarve.github.io/NLA/)
- [Matt Dunn's Second Brain](https://mattdunn.info/) - [A Pattern Language - Christopher Alexander (Architecture)](https://patternlanguage.cc/)
- [Pelayo Arbues' Notes](https://pelayoarbues.github.io/) - [oldwinter の数字花园](https://garden.oldwinter.top/)
- [Vince Imbat's Talahardin](https://vinceimbat.com/) - [Eilleen's Everything Notebook](https://quartz.eilleeenz.com/)
- [🧠🌳 Chad's Mind Garden](https://www.chadly.net/) - [🧠🌳 Chad's Mind Garden](https://www.chadly.net/)
- [Pedro MC Fernandes's Topo da Mente](https://www.pmcf.xyz/topo-da-mente/) - [Pedro MC Fernandes's Topo da Mente](https://www.pmcf.xyz/topo-da-mente/)
- [Mau Camargo's Notkesto](https://notes.camargomau.com/) - [Mau Camargo's Notkesto](https://notes.camargomau.com/)
- [Caicai's Novels](https://imoko.cc/blog/caicai/)
- [🌊 Collapsed Wave](https://collapsedwave.com/)
- [Sideny's 3D Artist's Handbook](https://sidney-eliot.github.io/3d-artists-handbook/) - [Sideny's 3D Artist's Handbook](https://sidney-eliot.github.io/3d-artists-handbook/)
- [Mike's AI Garden 🤖🪴](https://mwalton.me/)
- [Brandon Boswell's Garden](https://brandonkboswell.com) - [Brandon Boswell's Garden](https://brandonkboswell.com)
- [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/) - [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/)
- [Data Dictionary 🧠](https://glossary.airbyte.com/) - [Data Dictionary 🧠](https://glossary.airbyte.com/)
- [sspaeti.com's Second Brain](https://brain.sspaeti.com/) - [sspaeti.com's Second Brain](https://brain.sspaeti.com/)
- [🪴Aster's notebook](https://notes.asterhu.com) - [🪴Aster's notebook](https://notes.asterhu.com)
- [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja)
- [A Pattern Language - Christopher Alexander (Architecture)](https://patternlanguage.cc/)
- [Gatekeeper Wiki](https://www.gatekeeper.wiki) - [Gatekeeper Wiki](https://www.gatekeeper.wiki)
- [Ellie's Notes](https://ellie.wtf)
- [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja)
- [Eledah's Crystalline](https://blog.eledah.ir/)
- [🌓 Projects & Privacy - FOSS, tech, law](https://be-far.com)
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)! If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!

4
globals.d.ts vendored
View File

@@ -4,6 +4,10 @@ export declare global {
type: K, type: K,
listener: (this: Document, ev: CustomEventMap[K]) => void, listener: (this: Document, ev: CustomEventMap[K]) => void,
): void ): void
removeEventListener<K extends keyof CustomEventMap>(
type: K,
listener: (this: Document, ev: CustomEventMap[K]) => void,
): void
dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K] | UIEvent): void dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K] | UIEvent): void
} }
interface Window { interface Window {

867
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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.2.4", "version": "4.4.0",
"type": "module", "type": "module",
"author": "jackyzha0 <j.zhao2k19@gmail.com>", "author": "jackyzha0 <j.zhao2k19@gmail.com>",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
}, },
"engines": { "engines": {
"npm": ">=9.3.1", "npm": ">=9.3.1",
"node": ">=18.14" "node": "20 || >=22"
}, },
"keywords": [ "keywords": [
"site generator", "site generator",
@@ -36,8 +36,9 @@
}, },
"dependencies": { "dependencies": {
"@clack/prompts": "^0.7.0", "@clack/prompts": "^0.7.0",
"@floating-ui/dom": "^1.6.5", "@floating-ui/dom": "^1.6.10",
"@napi-rs/simple-git": "0.1.16", "@napi-rs/simple-git": "0.1.19",
"@tweenjs/tween.js": "^25.0.0",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
@@ -46,26 +47,27 @@
"esbuild-sass-plugin": "^2.16.1", "esbuild-sass-plugin": "^2.16.1",
"flexsearch": "0.7.43", "flexsearch": "0.7.43",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"globby": "^14.0.1", "globby": "^14.0.2",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"hast-util-to-html": "^9.0.1", "hast-util-to-html": "^9.0.2",
"hast-util-to-jsx-runtime": "^2.3.0", "hast-util-to-jsx-runtime": "^2.3.0",
"hast-util-to-string": "^3.0.0", "hast-util-to-string": "^3.0.0",
"is-absolute-url": "^4.0.1", "is-absolute-url": "^4.0.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lightningcss": "^1.25.1", "lightningcss": "^1.26.0",
"mdast-util-find-and-replace": "^3.0.1", "mdast-util-find-and-replace": "^3.0.1",
"mdast-util-to-hast": "^13.2.0", "mdast-util-to-hast": "^13.2.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.22.1", "pixi.js": "^8.3.4",
"preact-render-to-string": "^6.5.5", "preact": "^10.24.0",
"preact-render-to-string": "^6.5.11",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"pretty-time": "^1.1.0", "pretty-time": "^1.1.0",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
"rehype-citation": "^2.0.0", "rehype-citation": "^2.1.1",
"rehype-katex": "^7.0.0", "rehype-katex": "^7.0.1",
"rehype-mathjax": "^6.0.0", "rehype-mathjax": "^6.0.0",
"rehype-pretty-code": "^0.13.2", "rehype-pretty-code": "^0.13.2",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
@@ -79,16 +81,16 @@
"remark-rehype": "^11.1.0", "remark-rehype": "^11.1.0",
"remark-smartypants": "^3.0.2", "remark-smartypants": "^3.0.2",
"rfdc": "^1.4.1", "rfdc": "^1.4.1",
"rimraf": "^5.0.7", "rimraf": "^6.0.1",
"serve-handler": "^6.1.5", "serve-handler": "^6.1.5",
"shiki": "^1.10.3", "shiki": "^1.12.1",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"to-vfile": "^8.0.0", "to-vfile": "^8.0.0",
"toml": "^3.0.0", "toml": "^3.0.0",
"unified": "^11.0.4", "unified": "^11.0.5",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"vfile": "^6.0.1", "vfile": "^6.0.3",
"workerpool": "^9.1.2", "workerpool": "^9.1.3",
"ws": "^8.18.0", "ws": "^8.18.0",
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
@@ -97,14 +99,14 @@
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^20.12.5", "@types/node": "^22.5.5",
"@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.11", "@types/ws": "^8.5.12",
"@types/yargs": "^17.0.32", "@types/yargs": "^17.0.33",
"esbuild": "^0.19.9", "esbuild": "^0.19.9",
"prettier": "^3.3.2", "prettier": "^3.3.3",
"tsx": "^4.16.0", "tsx": "^4.19.0",
"typescript": "^5.5.3" "typescript": "^5.6.2"
} }
} }

View File

@@ -38,8 +38,13 @@ type BuildData = {
type FileEvent = "add" | "change" | "delete" type FileEvent = "add" | "change" | "delete"
function newBuildId() {
return Math.random().toString(36).substring(2, 8)
}
async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
const ctx: BuildCtx = { const ctx: BuildCtx = {
buildId: newBuildId(),
argv, argv,
cfg, cfg,
allSlugs: [], allSlugs: [],
@@ -157,10 +162,13 @@ async function partialRebuildFromEntrypoint(
return return
} }
const buildStart = new Date().getTime() const buildId = newBuildId()
buildData.lastBuildMs = buildStart ctx.buildId = buildId
buildData.lastBuildMs = new Date().getTime()
const release = await mut.acquire() const release = await mut.acquire()
if (buildData.lastBuildMs > buildStart) {
// if there's another build after us, release and let them do it
if (ctx.buildId !== buildId) {
release() release()
return return
} }
@@ -351,26 +359,22 @@ async function rebuildFromEntrypoint(
toRemove.add(filePath) toRemove.add(filePath)
} }
const buildStart = new Date().getTime() const buildId = newBuildId()
buildData.lastBuildMs = buildStart ctx.buildId = buildId
buildData.lastBuildMs = new Date().getTime()
const release = await mut.acquire() const release = await mut.acquire()
// there's another build after us, release and let them do it // there's another build after us, release and let them do it
if (buildData.lastBuildMs > buildStart) { if (ctx.buildId !== buildId) {
release() release()
return return
} }
const perf = new PerfTimer() const perf = new PerfTimer()
console.log(chalk.yellow("Detected change, rebuilding...")) console.log(chalk.yellow("Detected change, rebuilding..."))
try { try {
const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp)) const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp))
const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
.filter((fp) => !toRemove.has(fp))
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath))
ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]
const parsedContent = await parseMarkdown(ctx, filesToRebuild) const parsedContent = await parseMarkdown(ctx, filesToRebuild)
for (const content of parsedContent) { for (const content of parsedContent) {
const [_tree, vfile] = content const [_tree, vfile] = content
@@ -384,6 +388,13 @@ async function rebuildFromEntrypoint(
const parsedFiles = [...contentMap.values()] const parsedFiles = [...contentMap.values()]
const filteredContent = filterContent(ctx, parsedFiles) const filteredContent = filterContent(ctx, parsedFiles)
// re-update slugs
const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])]
.filter((fp) => !toRemove.has(fp))
.map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath))
ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]
// TODO: we can probably traverse the link graph to figure out what's safe to delete here // TODO: we can probably traverse the link graph to figure out what's safe to delete here
// instead of just deleting everything // instead of just deleting everything
await rimraf(path.join(argv.output, ".*"), { glob: true }) await rimraf(path.join(argv.output, ".*"), { glob: true })
@@ -396,10 +407,10 @@ async function rebuildFromEntrypoint(
} }
} }
release()
clientRefresh() clientRefresh()
toRebuild.clear() toRebuild.clear()
toRemove.clear() toRemove.clear()
release()
} }
export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => { export default async (argv: Argv, mut: Mutex, clientRefresh: () => void) => {

View File

@@ -1,4 +1,7 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang"
// @ts-ignore
import script from "./scripts/comments.inline"
type Options = { type Options = {
provider: "giscus" provider: "giscus"
@@ -19,48 +22,23 @@ function boolToStringBool(b: boolean): string {
} }
export default ((opts: Options) => { export default ((opts: Options) => {
const Comments: QuartzComponent = (_props: QuartzComponentProps) => <div class="giscus"></div> const Comments: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
Comments.afterDOMLoaded = ` return (
const giscusScript = document.createElement("script") <div
giscusScript.src = "https://giscus.app/client.js" class={classNames(displayClass, "giscus")}
giscusScript.async = true data-repo={opts.options.repo}
giscusScript.crossOrigin = "anonymous" data-repo-id={opts.options.repoId}
giscusScript.setAttribute("data-loading", "lazy") data-category={opts.options.category}
giscusScript.setAttribute("data-emit-metadata", "0") data-category-id={opts.options.categoryId}
giscusScript.setAttribute("data-repo", "${opts.options.repo}") data-mapping={opts.options.mapping ?? "url"}
giscusScript.setAttribute("data-repo-id", "${opts.options.repoId}") data-strict={boolToStringBool(opts.options.strict ?? true)}
giscusScript.setAttribute("data-category", "${opts.options.category}") data-reactions-enabled={boolToStringBool(opts.options.reactionsEnabled ?? true)}
giscusScript.setAttribute("data-category-id", "${opts.options.categoryId}") data-input-position={opts.options.inputPosition ?? "bottom"}
giscusScript.setAttribute("data-mapping", "${opts.options.mapping ?? "url"}") ></div>
giscusScript.setAttribute("data-strict", "${boolToStringBool(opts.options.strict ?? true)}") )
giscusScript.setAttribute("data-reactions-enabled", "${boolToStringBool(opts.options.reactionsEnabled ?? true)}") }
giscusScript.setAttribute("data-input-position", "${opts.options.inputPosition ?? "bottom"}")
const theme = document.documentElement.getAttribute("saved-theme") Comments.afterDOMLoaded = script
giscusScript.setAttribute("data-theme", theme)
document.head.appendChild(giscusScript)
const changeTheme = (e) => {
const theme = e.detail.theme
const iframe = document.querySelector('iframe.giscus-frame')
if (!iframe) {
return
}
iframe.contentWindow.postMessage({
giscus: {
setConfig: {
theme: theme
}
}
}, 'https://giscus.app')
}
document.addEventListener("nav", () => {
document.addEventListener("themechange", changeTheme)
window.addCleanup(() => document.removeEventListener("themechange", changeTheme))
})
`
return Comments return Comments
}) satisfies QuartzComponentConstructor<Options> }) satisfies QuartzComponentConstructor<Options>

View File

@@ -9,41 +9,38 @@ import { classNames } from "../util/lang"
const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
return ( return (
<div class={classNames(displayClass, "darkmode")}> <button class={classNames(displayClass, "darkmode")} id="darkmode">
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} /> <svg
<label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}> xmlns="http://www.w3.org/2000/svg"
<svg xmlnsXlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg" version="1.1"
xmlnsXlink="http://www.w3.org/1999/xlink" id="dayIcon"
version="1.1" x="0px"
id="dayIcon" y="0px"
x="0px" viewBox="0 0 35 35"
y="0px" style="enable-background:new 0 0 35 35"
viewBox="0 0 35 35" xmlSpace="preserve"
style="enable-background:new 0 0 35 35" aria-label={i18n(cfg.locale).components.themeToggle.darkMode}
xmlSpace="preserve" >
> <title>{i18n(cfg.locale).components.themeToggle.darkMode}</title>
<title>{i18n(cfg.locale).components.themeToggle.darkMode}</title> <path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path>
<path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z"></path> </svg>
</svg> <svg
</label> xmlns="http://www.w3.org/2000/svg"
<label id="toggle-label-dark" for="darkmode-toggle" tabIndex={-1}> xmlnsXlink="http://www.w3.org/1999/xlink"
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg" id="nightIcon"
xmlnsXlink="http://www.w3.org/1999/xlink" x="0px"
version="1.1" y="0px"
id="nightIcon" viewBox="0 0 100 100"
x="0px" style="enable-background:new 0 0 100 100"
y="0px" xmlSpace="preserve"
viewBox="0 0 100 100" aria-label={i18n(cfg.locale).components.themeToggle.lightMode}
style="enable-background:new 0 0 100 100" >
xmlSpace="preserve" <title>{i18n(cfg.locale).components.themeToggle.lightMode}</title>
> <path d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571 C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23 c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369 c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65 c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"></path>
<title>{i18n(cfg.locale).components.themeToggle.lightMode}</title> </svg>
<path d="M96.76,66.458c-0.853-0.852-2.15-1.064-3.23-0.534c-6.063,2.991-12.858,4.571-19.655,4.571 C62.022,70.495,50.88,65.88,42.5,57.5C29.043,44.043,25.658,23.536,34.076,6.47c0.532-1.08,0.318-2.379-0.534-3.23 c-0.851-0.852-2.15-1.064-3.23-0.534c-4.918,2.427-9.375,5.619-13.246,9.491c-9.447,9.447-14.65,22.008-14.65,35.369 c0,13.36,5.203,25.921,14.65,35.368s22.008,14.65,35.368,14.65c13.361,0,25.921-5.203,35.369-14.65 c3.872-3.871,7.064-8.328,9.491-13.246C97.826,68.608,97.611,67.309,96.76,66.458z"></path> </button>
</svg>
</label>
</div>
) )
} }

View File

@@ -44,12 +44,9 @@ export default ((userOpts?: Partial<Options>) => {
// memoized // memoized
let fileTree: FileNode let fileTree: FileNode
let jsonTree: string let jsonTree: string
let lastBuildId: string = ""
function constructFileTree(allFiles: QuartzPluginData[]) { function constructFileTree(allFiles: QuartzPluginData[]) {
if (fileTree) {
return
}
// Construct tree from allFiles // Construct tree from allFiles
fileTree = new FileNode("") fileTree = new FileNode("")
allFiles.forEach((file) => fileTree.add(file)) allFiles.forEach((file) => fileTree.add(file))
@@ -76,12 +73,17 @@ export default ((userOpts?: Partial<Options>) => {
} }
const Explorer: QuartzComponent = ({ const Explorer: QuartzComponent = ({
ctx,
cfg, cfg,
allFiles, allFiles,
displayClass, displayClass,
fileData, fileData,
}: QuartzComponentProps) => { }: QuartzComponentProps) => {
constructFileTree(allFiles) if (ctx.buildId !== lastBuildId) {
lastBuildId = ctx.buildId
constructFileTree(allFiles)
}
return ( return (
<div class={classNames(displayClass, "explorer")}> <div class={classNames(displayClass, "explorer")}>
<button <button
@@ -91,8 +93,10 @@ export default ((userOpts?: Partial<Options>) => {
data-collapsed={opts.folderDefaultState} data-collapsed={opts.folderDefaultState}
data-savestate={opts.useSavedState} data-savestate={opts.useSavedState}
data-tree={jsonTree} data-tree={jsonTree}
aria-controls="explorer-content"
aria-expanded={opts.folderDefaultState === "open"}
> >
<h1>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h1> <h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="14" width="14"

View File

@@ -65,31 +65,32 @@ export default ((opts?: GraphOptions) => {
<h3>{i18n(cfg.locale).components.graph.title}</h3> <h3>{i18n(cfg.locale).components.graph.title}</h3>
<div class="graph-outer"> <div class="graph-outer">
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div> <div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
<svg <button id="global-graph-icon" aria-label="Global Graph">
version="1.1" <svg
id="global-graph-icon" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink" xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px" x="0px"
y="0px" y="0px"
viewBox="0 0 55 55" viewBox="0 0 55 55"
fill="currentColor" fill="currentColor"
xmlSpace="preserve" xmlSpace="preserve"
> >
<path <path
d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17 d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17
s-3.542,0.634-4.898,1.688l-7.669-7.669C16.785,10.424,17,9.74,17,9c0-2.206-1.794-4-4-4S9,6.794,9,9s1.794,4,4,4 s-3.542,0.634-4.898,1.688l-7.669-7.669C16.785,10.424,17,9.74,17,9c0-2.206-1.794-4-4-4S9,6.794,9,9s1.794,4,4,4
c0.74,0,1.424-0.215,2.019-0.567l7.669,7.669C21.634,21.458,21,23.154,21,25s0.634,3.542,1.688,4.897L10.024,42.562 c0.74,0,1.424-0.215,2.019-0.567l7.669,7.669C21.634,21.458,21,23.154,21,25s0.634,3.542,1.688,4.897L10.024,42.562
C8.958,41.595,7.549,41,6,41c-3.309,0-6,2.691-6,6s2.691,6,6,6s6-2.691,6-6c0-1.035-0.263-2.009-0.726-2.86l12.829-12.829 C8.958,41.595,7.549,41,6,41c-3.309,0-6,2.691-6,6s2.691,6,6,6s6-2.691,6-6c0-1.035-0.263-2.009-0.726-2.86l12.829-12.829
c1.106,0.86,2.44,1.436,3.898,1.619v10.16c-2.833,0.478-5,2.942-5,5.91c0,3.309,2.691,6,6,6s6-2.691,6-6c0-2.967-2.167-5.431-5-5.91 c1.106,0.86,2.44,1.436,3.898,1.619v10.16c-2.833,0.478-5,2.942-5,5.91c0,3.309,2.691,6,6,6s6-2.691,6-6c0-2.967-2.167-5.431-5-5.91
v-10.16c1.458-0.183,2.792-0.759,3.898-1.619l7.669,7.669C41.215,39.576,41,40.26,41,41c0,2.206,1.794,4,4,4s4-1.794,4-4 v-10.16c1.458-0.183,2.792-0.759,3.898-1.619l7.669,7.669C41.215,39.576,41,40.26,41,41c0,2.206,1.794,4,4,4s4-1.794,4-4
s-1.794-4-4-4c-0.74,0-1.424,0.215-2.019,0.567l-7.669-7.669C36.366,28.542,37,26.846,37,25s-0.634-3.542-1.688-4.897l9.665-9.665 s-1.794-4-4-4c-0.74,0-1.424,0.215-2.019,0.567l-7.669-7.669C36.366,28.542,37,26.846,37,25s-0.634-3.542-1.688-4.897l9.665-9.665
C46.042,11.405,47.451,12,49,12c3.309,0,6-2.691,6-6S52.309,0,49,0z M11,9c0-1.103,0.897-2,2-2s2,0.897,2,2s-0.897,2-2,2 C46.042,11.405,47.451,12,49,12c3.309,0,6-2.691,6-6S52.309,0,49,0z M11,9c0-1.103,0.897-2,2-2s2,0.897,2,2s-0.897,2-2,2
S11,10.103,11,9z M6,51c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S8.206,51,6,51z M33,49c0,2.206-1.794,4-4,4s-4-1.794-4-4 S11,10.103,11,9z M6,51c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S8.206,51,6,51z M33,49c0,2.206-1.794,4-4,4s-4-1.794-4-4
s1.794-4,4-4S33,46.794,33,49z M29,31c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6S32.309,31,29,31z M47,41c0,1.103-0.897,2-2,2 s1.794-4,4-4S33,46.794,33,49z M29,31c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6S32.309,31,29,31z M47,41c0,1.103-0.897,2-2,2
s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z" s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z"
/> />
</svg> </svg>
</button>
</div> </div>
<div id="global-graph-outer"> <div id="global-graph-outer">
<div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div> <div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>

View File

@@ -46,11 +46,13 @@ export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort
return ( return (
<li class="section-li"> <li class="section-li">
<div class="section"> <div class="section">
{page.dates && ( <div>
<p class="meta"> {page.dates && (
<Date date={getDate(cfg, page)!} locale={cfg.locale} /> <p class="meta">
</p> <Date date={getDate(cfg, page)!} locale={cfg.locale} />
)} </p>
)}
</div>
<div class="desc"> <div class="desc">
<h3> <h3>
<a href={resolveRelative(fileData.slug!, page.slug!)} class="internal"> <a href={resolveRelative(fileData.slug!, page.slug!)} class="internal">

View File

@@ -7,14 +7,15 @@ const PageTitle: QuartzComponent = ({ fileData, cfg, displayClass }: QuartzCompo
const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title
const baseDir = pathToRoot(fileData.slug!) const baseDir = pathToRoot(fileData.slug!)
return ( return (
<h1 class={classNames(displayClass, "page-title")}> <h2 class={classNames(displayClass, "page-title")}>
<a href={baseDir}>{title}</a> <a href={baseDir}>{title}</a>
</h1> </h2>
) )
} }
PageTitle.css = ` PageTitle.css = `
.page-title { .page-title {
font-size: 1.75rem;
margin: 0; margin: 0;
} }
` `

View File

@@ -19,24 +19,16 @@ export default ((userOpts?: Partial<SearchOptions>) => {
const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder
return ( return (
<div class={classNames(displayClass, "search")}> <div class={classNames(displayClass, "search")}>
<div id="search-icon"> <button class="search-button" id="search-button">
<p>{i18n(cfg.locale).components.search.title}</p> <p>{i18n(cfg.locale).components.search.title}</p>
<div></div> <svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7">
<svg <title>Search</title>
tabIndex={0}
aria-labelledby="title desc"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 19.9 19.7"
>
<title id="title">Search</title>
<desc id="desc">Search</desc>
<g class="search-path" fill="none"> <g class="search-path" fill="none">
<path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" /> <path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4" />
<circle cx="8" cy="8" r="7" /> <circle cx="8" cy="8" r="7" />
</g> </g>
</svg> </svg>
</div> </button>
<div id="search-container"> <div id="search-container">
<div id="search-space"> <div id="search-space">
<input <input

View File

@@ -26,7 +26,13 @@ const TableOfContents: QuartzComponent = ({
return ( return (
<div class={classNames(displayClass, "toc")}> <div class={classNames(displayClass, "toc")}>
<button type="button" id="toc" class={fileData.collapseToc ? "collapsed" : ""}> <button
type="button"
id="toc"
class={fileData.collapseToc ? "collapsed" : ""}
aria-controls="toc-content"
aria-expanded={!fileData.collapseToc}
>
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3> <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -43,7 +49,7 @@ const TableOfContents: QuartzComponent = ({
<polyline points="6 9 12 15 18 9"></polyline> <polyline points="6 9 12 15 18 9"></polyline>
</svg> </svg>
</button> </button>
<div id="toc-content"> <div id="toc-content" class={fileData.collapseToc ? "collapsed" : ""}>
<ul class="overflow"> <ul class="overflow">
{fileData.toc.map((tocEntry) => ( {fileData.toc.map((tocEntry) => (
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}> <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>

View File

@@ -242,8 +242,8 @@ export function renderPage(
</div> </div>
</div> </div>
{RightComponent} {RightComponent}
<Footer {...componentData} />
</Body> </Body>
<Footer {...componentData} />
</div> </div>
</body> </body>
{pageResources.js {pageResources.js

View File

@@ -0,0 +1,67 @@
const changeTheme = (e: CustomEventMap["themechange"]) => {
const theme = e.detail.theme
const iframe = document.querySelector("iframe.giscus-frame") as HTMLIFrameElement
if (!iframe) {
return
}
if (!iframe.contentWindow) {
return
}
iframe.contentWindow.postMessage(
{
giscus: {
setConfig: {
theme: theme,
},
},
},
"https://giscus.app",
)
}
type GiscusElement = Omit<HTMLElement, "dataset"> & {
dataset: DOMStringMap & {
repo: `${string}/${string}`
repoId: string
category: string
categoryId: string
mapping: "url" | "title" | "og:title" | "specific" | "number" | "pathname"
strict: string
reactionsEnabled: string
inputPosition: "top" | "bottom"
}
}
document.addEventListener("nav", () => {
const giscusContainer = document.querySelector(".giscus") as GiscusElement
if (!giscusContainer) {
return
}
const giscusScript = document.createElement("script")
giscusScript.src = "https://giscus.app/client.js"
giscusScript.async = true
giscusScript.crossOrigin = "anonymous"
giscusScript.setAttribute("data-loading", "lazy")
giscusScript.setAttribute("data-emit-metadata", "0")
giscusScript.setAttribute("data-repo", giscusContainer.dataset.repo)
giscusScript.setAttribute("data-repo-id", giscusContainer.dataset.repoId)
giscusScript.setAttribute("data-category", giscusContainer.dataset.category)
giscusScript.setAttribute("data-category-id", giscusContainer.dataset.categoryId)
giscusScript.setAttribute("data-mapping", giscusContainer.dataset.mapping)
giscusScript.setAttribute("data-strict", giscusContainer.dataset.strict)
giscusScript.setAttribute("data-reactions-enabled", giscusContainer.dataset.reactionsEnabled)
giscusScript.setAttribute("data-input-position", giscusContainer.dataset.inputPosition)
const theme = document.documentElement.getAttribute("saved-theme")
if (theme) {
giscusScript.setAttribute("data-theme", theme)
}
giscusContainer.appendChild(giscusScript)
document.addEventListener("themechange", changeTheme)
window.addCleanup(() => document.removeEventListener("themechange", changeTheme))
})

View File

@@ -11,7 +11,8 @@ const emitThemeChangeEvent = (theme: "light" | "dark") => {
document.addEventListener("nav", () => { document.addEventListener("nav", () => {
const switchTheme = (e: Event) => { const switchTheme = (e: Event) => {
const newTheme = (e.target as HTMLInputElement)?.checked ? "dark" : "light" const newTheme =
document.documentElement.getAttribute("saved-theme") === "dark" ? "light" : "dark"
document.documentElement.setAttribute("saved-theme", newTheme) document.documentElement.setAttribute("saved-theme", newTheme)
localStorage.setItem("theme", newTheme) localStorage.setItem("theme", newTheme)
emitThemeChangeEvent(newTheme) emitThemeChangeEvent(newTheme)
@@ -21,17 +22,13 @@ document.addEventListener("nav", () => {
const newTheme = e.matches ? "dark" : "light" const newTheme = e.matches ? "dark" : "light"
document.documentElement.setAttribute("saved-theme", newTheme) document.documentElement.setAttribute("saved-theme", newTheme)
localStorage.setItem("theme", newTheme) localStorage.setItem("theme", newTheme)
toggleSwitch.checked = e.matches
emitThemeChangeEvent(newTheme) emitThemeChangeEvent(newTheme)
} }
// Darkmode toggle // Darkmode toggle
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement const themeButton = document.querySelector("#darkmode") as HTMLButtonElement
toggleSwitch.addEventListener("change", switchTheme) themeButton.addEventListener("click", switchTheme)
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme)) window.addCleanup(() => themeButton.removeEventListener("click", switchTheme))
if (currentTheme === "dark") {
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)")

View File

@@ -17,6 +17,10 @@ const observer = new IntersectionObserver((entries) => {
function toggleExplorer(this: HTMLElement) { function toggleExplorer(this: HTMLElement) {
this.classList.toggle("collapsed") this.classList.toggle("collapsed")
this.setAttribute(
"aria-expanded",
this.getAttribute("aria-expanded") === "true" ? "false" : "true",
)
const content = this.nextElementSibling as MaybeHTMLElement const content = this.nextElementSibling as MaybeHTMLElement
if (!content) return if (!content) return

View File

@@ -1,19 +1,56 @@
import type { ContentDetails, ContentIndex } from "../../plugins/emitters/contentIndex" import type { ContentDetails } from "../../plugins/emitters/contentIndex"
import * as d3 from "d3" import {
SimulationNodeDatum,
SimulationLinkDatum,
Simulation,
forceSimulation,
forceManyBody,
forceCenter,
forceLink,
forceCollide,
zoomIdentity,
select,
drag,
zoom,
} from "d3"
import { Text, Graphics, Application, Container, Circle } from "pixi.js"
import { Group as TweenGroup, Tween as Tweened } from "@tweenjs/tween.js"
import { registerEscapeHandler, removeAllChildren } from "./util" import { registerEscapeHandler, removeAllChildren } from "./util"
import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path" import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
import { D3Config } from "../Graph"
type GraphicsInfo = {
color: string
gfx: Graphics
alpha: number
active: boolean
}
type NodeData = { type NodeData = {
id: SimpleSlug id: SimpleSlug
text: string text: string
tags: string[] tags: string[]
} & d3.SimulationNodeDatum } & SimulationNodeDatum
type LinkData = { type SimpleLinkData = {
source: SimpleSlug source: SimpleSlug
target: SimpleSlug target: SimpleSlug
} }
type LinkData = {
source: NodeData
target: NodeData
} & SimulationLinkDatum<NodeData>
type LinkRenderData = GraphicsInfo & {
simulationData: LinkData
}
type NodeRenderData = GraphicsInfo & {
simulationData: NodeData
label: Text
}
const localStorageKey = "graph-visited" const localStorageKey = "graph-visited"
function getVisited(): Set<SimpleSlug> { function getVisited(): Set<SimpleSlug> {
return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]")) return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]"))
@@ -25,6 +62,11 @@ function addToVisited(slug: SimpleSlug) {
localStorage.setItem(localStorageKey, JSON.stringify([...visited])) localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
} }
type TweenNode = {
update: (time: number) => void
stop: () => void
}
async function renderGraph(container: string, fullSlug: FullSlug) { async function renderGraph(container: string, fullSlug: FullSlug) {
const slug = simplifySlug(fullSlug) const slug = simplifySlug(fullSlug)
const visited = getVisited() const visited = getVisited()
@@ -45,7 +87,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
removeTags, removeTags,
showTags, showTags,
focusOnHover, focusOnHover,
} = JSON.parse(graph.dataset["cfg"]!) } = JSON.parse(graph.dataset["cfg"]!) as D3Config
const data: Map<SimpleSlug, ContentDetails> = new Map( const data: Map<SimpleSlug, ContentDetails> = new Map(
Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [ Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [
@@ -53,10 +95,11 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
v, v,
]), ]),
) )
const links: LinkData[] = [] const links: SimpleLinkData[] = []
const tags: SimpleSlug[] = [] const tags: SimpleSlug[] = []
const validLinks = new Set(data.keys()) const validLinks = new Set(data.keys())
const tweens = new Map<string, TweenNode>()
for (const [source, details] of data.entries()) { for (const [source, details] of data.entries()) {
const outgoing = details.links ?? [] const outgoing = details.links ?? []
@@ -100,263 +143,406 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
if (showTags) tags.forEach((tag) => neighbourhood.add(tag)) if (showTags) tags.forEach((tag) => neighbourhood.add(tag))
} }
const nodes = [...neighbourhood].map((url) => {
const text = url.startsWith("tags/") ? "#" + url.substring(5) : (data.get(url)?.title ?? url)
return {
id: url,
text,
tags: data.get(url)?.tags ?? [],
}
})
const graphData: { nodes: NodeData[]; links: LinkData[] } = { const graphData: { nodes: NodeData[]; links: LinkData[] } = {
nodes: [...neighbourhood].map((url) => { nodes,
const text = url.startsWith("tags/") ? "#" + url.substring(5) : data.get(url)?.title ?? url links: links
return { .filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target))
id: url, .map((l) => ({
text: text, source: nodes.find((n) => n.id === l.source)!,
tags: data.get(url)?.tags ?? [], target: nodes.find((n) => n.id === l.target)!,
} })),
}),
links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)),
} }
const simulation: d3.Simulation<NodeData, LinkData> = d3 // we virtualize the simulation and use pixi to actually render it
.forceSimulation(graphData.nodes) const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
.force("charge", d3.forceManyBody().strength(-100 * repelForce)) .force("charge", forceManyBody().strength(-100 * repelForce))
.force( .force("center", forceCenter().strength(centerForce))
"link", .force("link", forceLink(graphData.links).distance(linkDistance))
d3 .force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
.forceLink(graphData.links)
.id((d: any) => d.id)
.distance(linkDistance),
)
.force("center", d3.forceCenter().strength(centerForce))
const height = Math.max(graph.offsetHeight, 250)
const width = graph.offsetWidth const width = graph.offsetWidth
const height = Math.max(graph.offsetHeight, 250)
const svg = d3 // precompute style prop strings as pixi doesn't support css variables
.select<HTMLElement, NodeData>("#" + container) const cssVars = [
.append("svg") "--secondary",
.attr("width", width) "--tertiary",
.attr("height", height) "--gray",
.attr("viewBox", [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale]) "--light",
"--lightgray",
// draw links between nodes "--dark",
const link = svg "--darkgray",
.append("g") "--bodyFont",
.selectAll("line") ] as const
.data(graphData.links) const computedStyleMap = cssVars.reduce(
.join("line") (acc, key) => {
.attr("class", "link") acc[key] = getComputedStyle(document.documentElement).getPropertyValue(key)
.attr("stroke", "var(--lightgray)") return acc
.attr("stroke-width", 1) },
{} as Record<(typeof cssVars)[number], string>,
// svg groups )
const graphNode = svg.append("g").selectAll("g").data(graphData.nodes).enter().append("g")
// calculate color // calculate color
const color = (d: NodeData) => { const color = (d: NodeData) => {
const isCurrent = d.id === slug const isCurrent = d.id === slug
if (isCurrent) { if (isCurrent) {
return "var(--secondary)" return computedStyleMap["--secondary"]
} else if (visited.has(d.id) || d.id.startsWith("tags/")) { } else if (visited.has(d.id) || d.id.startsWith("tags/")) {
return "var(--tertiary)" return computedStyleMap["--tertiary"]
} else { } else {
return "var(--gray)" return computedStyleMap["--gray"]
} }
} }
const drag = (simulation: d3.Simulation<NodeData, LinkData>) => {
function dragstarted(event: any, d: NodeData) {
if (!event.active) simulation.alphaTarget(1).restart()
d.fx = d.x
d.fy = d.y
}
function dragged(event: any, d: NodeData) {
d.fx = event.x
d.fy = event.y
}
function dragended(event: any, d: NodeData) {
if (!event.active) simulation.alphaTarget(0)
d.fx = null
d.fy = null
}
const noop = () => {}
return d3
.drag<Element, NodeData>()
.on("start", enableDrag ? dragstarted : noop)
.on("drag", enableDrag ? dragged : noop)
.on("end", enableDrag ? dragended : noop)
}
function nodeRadius(d: NodeData) { function nodeRadius(d: NodeData) {
const numLinks = links.filter((l: any) => l.source.id === d.id || l.target.id === d.id).length const numLinks = graphData.links.filter(
(l) => l.source.id === d.id || l.target.id === d.id,
).length
return 2 + Math.sqrt(numLinks) return 2 + Math.sqrt(numLinks)
} }
let connectedNodes: SimpleSlug[] = [] let hoveredNodeId: string | null = null
let hoveredNeighbours: Set<string> = new Set()
const linkRenderData: LinkRenderData[] = []
const nodeRenderData: NodeRenderData[] = []
function updateHoverInfo(newHoveredId: string | null) {
hoveredNodeId = newHoveredId
// draw individual nodes if (newHoveredId === null) {
const node = graphNode hoveredNeighbours = new Set()
.append("circle") for (const n of nodeRenderData) {
.attr("class", "node") n.active = false
.attr("id", (d) => d.id)
.attr("r", nodeRadius)
.attr("fill", color)
.style("cursor", "pointer")
.on("click", (_, d) => {
const targ = resolveRelative(fullSlug, d.id)
window.spaNavigate(new URL(targ, window.location.toString()))
})
.on("mouseover", function (_, d) {
const currentId = d.id
const linkNodes = d3
.selectAll(".link")
.filter((d: any) => d.source.id === currentId || d.target.id === currentId)
if (focusOnHover) {
// fade out non-neighbour nodes
connectedNodes = linkNodes.data().flatMap((d: any) => [d.source.id, d.target.id])
d3.selectAll<HTMLElement, NodeData>(".link")
.transition()
.duration(200)
.style("opacity", 0.2)
d3.selectAll<HTMLElement, NodeData>(".node")
.filter((d) => !connectedNodes.includes(d.id))
.transition()
.duration(200)
.style("opacity", 0.2)
d3.selectAll<HTMLElement, NodeData>(".node")
.filter((d) => !connectedNodes.includes(d.id))
.nodes()
.map((it) => d3.select(it.parentNode as HTMLElement).select("text"))
.forEach((it) => {
let opacity = parseFloat(it.style("opacity"))
it.transition()
.duration(200)
.attr("opacityOld", opacity)
.style("opacity", Math.min(opacity, 0.2))
})
} }
// highlight links for (const l of linkRenderData) {
linkNodes.transition().duration(200).attr("stroke", "var(--gray)").attr("stroke-width", 1) l.active = false
const bigFont = fontSize * 1.5
// show text for self
const parent = this.parentNode as HTMLElement
d3.select<HTMLElement, NodeData>(parent)
.raise()
.select("text")
.transition()
.duration(200)
.attr("opacityOld", d3.select(parent).select("text").style("opacity"))
.style("opacity", 1)
.style("font-size", bigFont + "em")
})
.on("mouseleave", function (_, d) {
if (focusOnHover) {
d3.selectAll<HTMLElement, NodeData>(".link").transition().duration(200).style("opacity", 1)
d3.selectAll<HTMLElement, NodeData>(".node").transition().duration(200).style("opacity", 1)
d3.selectAll<HTMLElement, NodeData>(".node")
.filter((d) => !connectedNodes.includes(d.id))
.nodes()
.map((it) => d3.select(it.parentNode as HTMLElement).select("text"))
.forEach((it) => it.transition().duration(200).style("opacity", it.attr("opacityOld")))
} }
const currentId = d.id } else {
const linkNodes = d3 hoveredNeighbours = new Set()
.selectAll(".link") for (const l of linkRenderData) {
.filter((d: any) => d.source.id === currentId || d.target.id === currentId) const linkData = l.simulationData
if (linkData.source.id === newHoveredId || linkData.target.id === newHoveredId) {
hoveredNeighbours.add(linkData.source.id)
hoveredNeighbours.add(linkData.target.id)
}
linkNodes.transition().duration(200).attr("stroke", "var(--lightgray)") l.active = linkData.source.id === newHoveredId || linkData.target.id === newHoveredId
}
const parent = this.parentNode as HTMLElement for (const n of nodeRenderData) {
d3.select<HTMLElement, NodeData>(parent) n.active = hoveredNeighbours.has(n.simulationData.id)
.select("text") }
.transition() }
.duration(200) }
.style("opacity", d3.select(parent).select("text").attr("opacityOld"))
.style("font-size", fontSize + "em") let dragStartTime = 0
let dragging = false
function renderLinks() {
tweens.get("link")?.stop()
const tweenGroup = new TweenGroup()
for (const l of linkRenderData) {
let alpha = 1
// if we are hovering over a node, we want to highlight the immediate neighbours
// with full alpha and the rest with default alpha
if (hoveredNodeId) {
alpha = l.active ? 1 : 0.2
}
l.color = l.active ? computedStyleMap["--gray"] : computedStyleMap["--lightgray"]
tweenGroup.add(new Tweened<LinkRenderData>(l).to({ alpha }, 200))
}
tweenGroup.getAll().forEach((tw) => tw.start())
tweens.set("link", {
update: tweenGroup.update.bind(tweenGroup),
stop() {
tweenGroup.getAll().forEach((tw) => tw.stop())
},
}) })
// @ts-ignore }
.call(drag(simulation))
// make tags hollow circles function renderLabels() {
node tweens.get("label")?.stop()
.filter((d) => d.id.startsWith("tags/")) const tweenGroup = new TweenGroup()
.attr("stroke", color)
.attr("stroke-width", 2)
.attr("fill", "var(--light)")
// draw labels const defaultScale = 1 / scale
const labels = graphNode const activeScale = defaultScale * 1.1
.append("text") for (const n of nodeRenderData) {
.attr("dx", 0) const nodeId = n.simulationData.id
.attr("dy", (d) => -nodeRadius(d) + "px")
.attr("text-anchor", "middle") if (hoveredNodeId === nodeId) {
.text((d) => d.text) tweenGroup.add(
.style("opacity", (opacityScale - 1) / 3.75) new Tweened<Text>(n.label).to(
.style("pointer-events", "none") {
.style("font-size", fontSize + "em") alpha: 1,
.raise() scale: { x: activeScale, y: activeScale },
// @ts-ignore },
.call(drag(simulation)) 100,
),
)
} else {
tweenGroup.add(
new Tweened<Text>(n.label).to(
{
alpha: n.label.alpha,
scale: { x: defaultScale, y: defaultScale },
},
100,
),
)
}
}
tweenGroup.getAll().forEach((tw) => tw.start())
tweens.set("label", {
update: tweenGroup.update.bind(tweenGroup),
stop() {
tweenGroup.getAll().forEach((tw) => tw.stop())
},
})
}
function renderNodes() {
tweens.get("hover")?.stop()
const tweenGroup = new TweenGroup()
for (const n of nodeRenderData) {
let alpha = 1
// if we are hovering over a node, we want to highlight the immediate neighbours
if (hoveredNodeId !== null && focusOnHover) {
alpha = n.active ? 1 : 0.2
}
tweenGroup.add(new Tweened<Graphics>(n.gfx, tweenGroup).to({ alpha }, 200))
}
tweenGroup.getAll().forEach((tw) => tw.start())
tweens.set("hover", {
update: tweenGroup.update.bind(tweenGroup),
stop() {
tweenGroup.getAll().forEach((tw) => tw.stop())
},
})
}
function renderPixiFromD3() {
renderNodes()
renderLinks()
renderLabels()
}
tweens.forEach((tween) => tween.stop())
tweens.clear()
const app = new Application()
await app.init({
width,
height,
antialias: true,
autoStart: false,
autoDensity: true,
backgroundAlpha: 0,
preference: "webgpu",
resolution: window.devicePixelRatio,
eventMode: "static",
})
graph.appendChild(app.canvas)
const stage = app.stage
stage.interactive = false
const labelsContainer = new Container<Text>({ zIndex: 3 })
const nodesContainer = new Container<Graphics>({ zIndex: 2 })
const linkContainer = new Container<Graphics>({ zIndex: 1 })
stage.addChild(nodesContainer, labelsContainer, linkContainer)
for (const n of graphData.nodes) {
const nodeId = n.id
const label = new Text({
interactive: false,
eventMode: "none",
text: n.text,
alpha: 0,
anchor: { x: 0.5, y: 1.2 },
style: {
fontSize: fontSize * 15,
fill: computedStyleMap["--dark"],
fontFamily: computedStyleMap["--bodyFont"],
},
resolution: window.devicePixelRatio * 4,
})
label.scale.set(1 / scale)
let oldLabelOpacity = 0
const isTagNode = nodeId.startsWith("tags/")
const gfx = new Graphics({
interactive: true,
label: nodeId,
eventMode: "static",
hitArea: new Circle(0, 0, nodeRadius(n)),
cursor: "pointer",
})
.circle(0, 0, nodeRadius(n))
.fill({ color: isTagNode ? computedStyleMap["--light"] : color(n) })
.stroke({ width: isTagNode ? 2 : 0, color: color(n) })
.on("pointerover", (e) => {
updateHoverInfo(e.target.label)
oldLabelOpacity = label.alpha
if (!dragging) {
renderPixiFromD3()
}
})
.on("pointerleave", () => {
updateHoverInfo(null)
label.alpha = oldLabelOpacity
if (!dragging) {
renderPixiFromD3()
}
})
nodesContainer.addChild(gfx)
labelsContainer.addChild(label)
const nodeRenderDatum: NodeRenderData = {
simulationData: n,
gfx,
label,
color: color(n),
alpha: 1,
active: false,
}
nodeRenderData.push(nodeRenderDatum)
}
for (const l of graphData.links) {
const gfx = new Graphics({ interactive: false, eventMode: "none" })
linkContainer.addChild(gfx)
const linkRenderDatum: LinkRenderData = {
simulationData: l,
gfx,
color: computedStyleMap["--lightgray"],
alpha: 1,
active: false,
}
linkRenderData.push(linkRenderDatum)
}
let currentTransform = zoomIdentity
if (enableDrag) {
select<HTMLCanvasElement, NodeData | undefined>(app.canvas).call(
drag<HTMLCanvasElement, NodeData | undefined>()
.container(() => app.canvas)
.subject(() => graphData.nodes.find((n) => n.id === hoveredNodeId))
.on("start", function dragstarted(event) {
if (!event.active) simulation.alphaTarget(1).restart()
event.subject.fx = event.subject.x
event.subject.fy = event.subject.y
event.subject.__initialDragPos = {
x: event.subject.x,
y: event.subject.y,
fx: event.subject.fx,
fy: event.subject.fy,
}
dragStartTime = Date.now()
dragging = true
})
.on("drag", function dragged(event) {
const initPos = event.subject.__initialDragPos
event.subject.fx = initPos.x + (event.x - initPos.x) / currentTransform.k
event.subject.fy = initPos.y + (event.y - initPos.y) / currentTransform.k
})
.on("end", function dragended(event) {
if (!event.active) simulation.alphaTarget(0)
event.subject.fx = null
event.subject.fy = null
dragging = false
// if the time between mousedown and mouseup is short, we consider it a click
if (Date.now() - dragStartTime < 500) {
const node = graphData.nodes.find((n) => n.id === event.subject.id) as NodeData
const targ = resolveRelative(fullSlug, node.id)
window.spaNavigate(new URL(targ, window.location.toString()))
}
}),
)
} else {
for (const node of nodeRenderData) {
node.gfx.on("click", () => {
const targ = resolveRelative(fullSlug, node.simulationData.id)
window.spaNavigate(new URL(targ, window.location.toString()))
})
}
}
// set panning
if (enableZoom) { if (enableZoom) {
svg.call( select<HTMLCanvasElement, NodeData>(app.canvas).call(
d3 zoom<HTMLCanvasElement, NodeData>()
.zoom<SVGSVGElement, NodeData>()
.extent([ .extent([
[0, 0], [0, 0],
[width, height], [width, height],
]) ])
.scaleExtent([0.25, 4]) .scaleExtent([0.25, 4])
.on("zoom", ({ transform }) => { .on("zoom", ({ transform }) => {
link.attr("transform", transform) currentTransform = transform
node.attr("transform", transform) stage.scale.set(transform.k, transform.k)
stage.position.set(transform.x, transform.y)
// zoom adjusts opacity of labels too
const scale = transform.k * opacityScale const scale = transform.k * opacityScale
const scaledOpacity = Math.max((scale - 1) / 3.75, 0) let scaleOpacity = Math.max((scale - 1) / 3.75, 0)
labels.attr("transform", transform).style("opacity", scaledOpacity) const activeNodes = nodeRenderData.filter((n) => n.active).flatMap((n) => n.label)
for (const label of labelsContainer.children) {
if (!activeNodes.includes(label)) {
label.alpha = scaleOpacity
}
}
}), }),
) )
} }
// progress the simulation function animate(time: number) {
simulation.on("tick", () => { for (const n of nodeRenderData) {
link const { x, y } = n.simulationData
.attr("x1", (d: any) => d.source.x) if (!x || !y) continue
.attr("y1", (d: any) => d.source.y) n.gfx.position.set(x + width / 2, y + height / 2)
.attr("x2", (d: any) => d.target.x) if (n.label) {
.attr("y2", (d: any) => d.target.y) n.label.position.set(x + width / 2, y + height / 2)
node.attr("cx", (d: any) => d.x).attr("cy", (d: any) => d.y) }
labels.attr("x", (d: any) => d.x).attr("y", (d: any) => d.y)
})
}
function renderGlobalGraph() {
const slug = getFullSlug(window)
const container = document.getElementById("global-graph-outer")
const sidebar = container?.closest(".sidebar") as HTMLElement
container?.classList.add("active")
if (sidebar) {
sidebar.style.zIndex = "1"
}
renderGraph("global-graph-container", slug)
function hideGlobalGraph() {
container?.classList.remove("active")
const graph = document.getElementById("global-graph-container")
if (sidebar) {
sidebar.style.zIndex = "unset"
} }
if (!graph) return
removeAllChildren(graph) for (const l of linkRenderData) {
const linkData = l.simulationData
l.gfx.clear()
l.gfx.moveTo(linkData.source.x! + width / 2, linkData.source.y! + height / 2)
l.gfx
.lineTo(linkData.target.x! + width / 2, linkData.target.y! + height / 2)
.stroke({ alpha: l.alpha, width: 1, color: l.color })
}
tweens.forEach((t) => t.update(time))
app.renderer.render(stage)
requestAnimationFrame(animate)
} }
registerEscapeHandler(container, hideGlobalGraph) const graphAnimationFrameHandle = requestAnimationFrame(animate)
window.addCleanup(() => cancelAnimationFrame(graphAnimationFrameHandle))
} }
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
@@ -364,7 +550,52 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
addToVisited(simplifySlug(slug)) addToVisited(simplifySlug(slug))
await renderGraph("graph-container", slug) await renderGraph("graph-container", slug)
// Function to re-render the graph when the theme changes
const handleThemeChange = () => {
renderGraph("graph-container", slug)
}
// event listener for theme change
document.addEventListener("themechange", handleThemeChange)
// cleanup for the event listener
window.addCleanup(() => {
document.removeEventListener("themechange", handleThemeChange)
})
const container = document.getElementById("global-graph-outer")
const sidebar = container?.closest(".sidebar") as HTMLElement
function renderGlobalGraph() {
const slug = getFullSlug(window)
container?.classList.add("active")
if (sidebar) {
sidebar.style.zIndex = "1"
}
renderGraph("global-graph-container", slug)
registerEscapeHandler(container, hideGlobalGraph)
}
function hideGlobalGraph() {
container?.classList.remove("active")
if (sidebar) {
sidebar.style.zIndex = "unset"
}
}
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
if (e.key === "g" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
e.preventDefault()
const globalGraphOpen = container?.classList.contains("active")
globalGraphOpen ? hideGlobalGraph() : renderGlobalGraph()
}
}
const containerIcon = document.getElementById("global-graph-icon") const containerIcon = document.getElementById("global-graph-icon")
containerIcon?.addEventListener("click", renderGlobalGraph) containerIcon?.addEventListener("click", renderGlobalGraph)
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph)) window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
document.addEventListener("keydown", shortcutHandler)
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
}) })

View File

@@ -148,7 +148,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
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
const searchIcon = document.getElementById("search-icon") const searchButton = document.getElementById("search-button")
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
const searchLayout = document.getElementById("search-layout") const searchLayout = document.getElementById("search-layout")
const idDataMap = Object.keys(data) as FullSlug[] const idDataMap = Object.keys(data) as FullSlug[]
@@ -191,6 +191,8 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
} }
searchType = "basic" // reset search type after closing searchType = "basic" // reset search type after closing
searchButton?.focus()
} }
function showSearch(searchTypeNew: SearchType) { function showSearch(searchTypeNew: SearchType) {
@@ -458,8 +460,8 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
document.addEventListener("keydown", shortcutHandler) document.addEventListener("keydown", shortcutHandler)
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler)) window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
searchIcon?.addEventListener("click", () => showSearch("basic")) searchButton?.addEventListener("click", () => showSearch("basic"))
window.addCleanup(() => searchIcon?.removeEventListener("click", () => showSearch("basic"))) window.addCleanup(() => searchButton?.removeEventListener("click", () => showSearch("basic")))
searchBar?.addEventListener("input", onType) searchBar?.addEventListener("input", onType)
window.addCleanup(() => searchBar?.removeEventListener("input", onType)) window.addCleanup(() => searchBar?.removeEventListener("input", onType))

View File

@@ -16,6 +16,10 @@ const observer = new IntersectionObserver((entries) => {
function toggleToc(this: HTMLElement) { function toggleToc(this: HTMLElement) {
this.classList.toggle("collapsed") this.classList.toggle("collapsed")
this.setAttribute(
"aria-expanded",
this.getAttribute("aria-expanded") === "true" ? "false" : "true",
)
const content = this.nextElementSibling as HTMLElement | undefined const content = this.nextElementSibling as HTMLElement | undefined
if (!content) return if (!content) return
content.classList.toggle("collapsed") content.classList.toggle("collapsed")

View File

@@ -3,6 +3,7 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb:
function click(this: HTMLElement, e: HTMLElementEventMap["click"]) { function click(this: HTMLElement, e: HTMLElementEventMap["click"]) {
if (e.target !== this) return if (e.target !== this) return
e.preventDefault() e.preventDefault()
e.stopPropagation()
cb() cb()
} }

View File

@@ -1,19 +1,50 @@
@use "../../styles/variables.scss" as *;
.backlinks { .backlinks {
position: relative; @media all and not ($desktop) {
overflow-y: auto;
display: initial;
&:after {
pointer-events: none;
content: "";
width: 100%;
height: 50px;
position: absolute;
left: 0;
bottom: 0;
opacity: 1;
transition: opacity 0.3s ease;
background: linear-gradient(transparent 0px, var(--light));
}
& > h3 { &:has(> .overflow) {
font-size: 1rem; position: unset;
margin: 0; }
}
& > ul { & > h3 {
list-style: none; font-size: 1rem;
padding: 0; margin: 0;
margin: 0.5rem 0; }
& > li { & > ul {
& > a { list-style: none;
background-color: transparent; padding: 0;
margin: 0.5rem 0;
& > li {
& > a {
background-color: transparent;
}
}
}
& > .overflow {
max-height: unset;
& > li:last-of-type {
margin-bottom: 0;
}
&:after {
display: none;
} }
} }
} }

View File

@@ -1,17 +1,15 @@
.darkmode { .darkmode {
cursor: pointer;
padding: 0;
position: relative; position: relative;
background: none;
border: none;
width: 20px; width: 20px;
height: 20px; height: 20px;
margin: 0 10px; margin: 0 10px;
text-align: inherit;
& > .toggle {
display: none;
box-sizing: border-box;
}
& svg { & svg {
cursor: pointer;
opacity: 0;
position: absolute; position: absolute;
width: 20px; width: 20px;
height: 20px; height: 20px;
@@ -29,20 +27,20 @@
color-scheme: light; color-scheme: light;
} }
:root[saved-theme="dark"] .toggle ~ label { :root[saved-theme="dark"] .darkmode {
& > #dayIcon { & > #dayIcon {
opacity: 0; display: none;
} }
& > #nightIcon { & > #nightIcon {
opacity: 1; display: inline;
} }
} }
:root .toggle ~ label { :root .darkmode {
& > #dayIcon { & > #dayIcon {
opacity: 1; display: inline;
} }
& > #nightIcon { & > #nightIcon {
opacity: 0; display: none;
} }
} }

View File

@@ -1,7 +1,24 @@
@use "../../styles/variables.scss" as *; @use "../../styles/variables.scss" as *;
.explorer {
&.desktop-only {
overflow-y: auto;
}
&:after {
pointer-events: none;
content: "";
width: 100%;
height: 50px;
position: absolute;
left: 0;
bottom: 0;
opacity: 1;
transition: opacity 0.3s ease;
background: linear-gradient(transparent 0px, var(--light));
}
}
button#explorer { button#explorer {
all: unset;
background-color: transparent; background-color: transparent;
border: none; border: none;
text-align: left; text-align: left;
@@ -11,7 +28,7 @@ button#explorer {
display: flex; display: flex;
align-items: center; align-items: center;
& h1 { & h2 {
font-size: 1rem; font-size: 1rem;
display: inline-block; display: inline-block;
margin: 0; margin: 0;
@@ -46,11 +63,17 @@ button#explorer {
list-style: none; list-style: none;
overflow: hidden; overflow: hidden;
max-height: none; max-height: none;
transition: max-height 0.35s ease; transition:
max-height 0.35s ease,
visibility 0s linear 0s;
margin-top: 0.5rem; margin-top: 0.5rem;
visibility: visible;
&.collapsed > .overflow::after { &.collapsed {
opacity: 0; transition:
max-height 0.35s ease,
visibility 0s linear 0.35s;
visibility: hidden;
} }
& ul { & ul {
@@ -67,6 +90,9 @@ button#explorer {
pointer-events: all; pointer-events: all;
} }
} }
> #explorer-ul {
max-height: none;
}
} }
svg { svg {

View File

@@ -16,10 +16,13 @@
overflow: hidden; overflow: hidden;
& > #global-graph-icon { & > #global-graph-icon {
cursor: pointer;
background: none;
border: none;
color: var(--dark); color: var(--dark);
opacity: 0.5; opacity: 0.5;
width: 18px; width: 24px;
height: 18px; height: 24px;
position: absolute; position: absolute;
padding: 0.2rem; padding: 0.2rem;
margin: 0.3rem; margin: 0.3rem;
@@ -59,10 +62,10 @@
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
height: 60vh; height: 80vh;
width: 50vw; width: 80vw;
@media all and (max-width: $fullPageWidth) { @media all and ($desktop) {
width: 90%; width: 90%;
} }
} }

View File

@@ -13,7 +13,7 @@ li.section-li {
display: grid; display: grid;
grid-template-columns: fit-content(8em) 3fr 1fr; grid-template-columns: fit-content(8em) 3fr 1fr;
@media all and (max-width: $mobileBreakpoint) { @media all and ($mobile) {
& > .tags { & > .tags {
display: none; display: none;
} }
@@ -23,7 +23,7 @@ li.section-li {
background-color: transparent; background-color: transparent;
} }
& > .meta { & .meta {
margin: 0 1em 0 0; margin: 0 1em 0 0;
opacity: 0.6; opacity: 0.6;
} }

View File

@@ -70,7 +70,7 @@
opacity 0.3s ease, opacity 0.3s ease,
visibility 0.3s ease; visibility 0.3s ease;
@media all and (max-width: $mobileBreakpoint) { @media all and ($mobile) {
display: none !important; display: none !important;
} }
} }

View File

@@ -3,20 +3,25 @@
.search { .search {
min-width: fit-content; min-width: fit-content;
max-width: 14rem; max-width: 14rem;
flex-grow: 0.3; @media all and ($mobile) {
flex-grow: 0.3;
}
& > #search-icon { & > .search-button {
background-color: var(--lightgray); background-color: var(--lightgray);
border: none;
border-radius: 4px; border-radius: 4px;
font-family: inherit;
font-size: inherit;
height: 2rem; height: 2rem;
padding: 0;
display: flex; display: flex;
align-items: center; align-items: center;
text-align: inherit;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
width: 100%;
& > div { justify-content: space-between;
flex-grow: 1;
}
& > p { & > p {
display: inline; display: inline;
@@ -59,7 +64,7 @@
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@media all and (max-width: $fullPageWidth) { @media all and ($desktop) {
width: 90%; width: 90%;
} }
@@ -101,7 +106,7 @@
flex: 0 0 min(30%, 450px); flex: 0 0 min(30%, 450px);
} }
@media all and (min-width: $tabletBreakpoint) { @media all and not ($tablet) {
&[data-preview] { &[data-preview] {
& .result-card > p.preview { & .result-card > p.preview {
display: none; display: none;
@@ -127,7 +132,7 @@
border-radius: 5px; border-radius: 5px;
} }
@media all and (max-width: $tabletBreakpoint) { @media all and ($tablet) {
& > #preview-container { & > #preview-container {
display: none !important; display: none !important;
} }

View File

@@ -1,3 +1,7 @@
.toc {
overflow-y: auto;
}
button#toc { button#toc {
background-color: transparent; background-color: transparent;
border: none; border: none;
@@ -29,8 +33,18 @@ button#toc {
list-style: none; list-style: none;
overflow: hidden; overflow: hidden;
max-height: none; max-height: none;
transition: max-height 0.5s ease; transition:
max-height 0.5s ease,
visibility 0s linear 0s;
position: relative; position: relative;
visibility: visible;
&.collapsed {
transition:
max-height 0.5s ease,
visibility 0s linear 0.5s;
visibility: hidden;
}
&.collapsed > .overflow::after { &.collapsed > .overflow::after {
opacity: 0; opacity: 0;

View File

@@ -147,7 +147,7 @@ function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentReso
} else if (cfg.analytics?.provider === "cabin") { } else if (cfg.analytics?.provider === "cabin") {
componentResources.afterDOMLoaded.push(` componentResources.afterDOMLoaded.push(`
const cabinScript = document.createElement("script") const cabinScript = document.createElement("script")
cabinScript.src = "${cfg.analytics.host ?? "https://scripts.cabin.dev"}/cabin.js" cabinScript.src = "${cfg.analytics.host ?? "https://scripts.withcabin.com"}/hello.js"
cabinScript.defer = true cabinScript.defer = true
cabinScript.async = true cabinScript.async = true
document.head.appendChild(cabinScript) document.head.appendChild(cabinScript)

View File

@@ -28,10 +28,10 @@ export function getStaticResourcesFromPlugins(ctx: BuildCtx) {
loadTime: "afterDOMReady", loadTime: "afterDOMReady",
contentType: "inline", contentType: "inline",
script: ` script: `
const socket = new WebSocket('${wsUrl}') const socket = new WebSocket('${wsUrl}')
// reload(true) ensures resources like images and scripts are fetched again in firefox // reload(true) ensures resources like images and scripts are fetched again in firefox
socket.addEventListener('message', () => document.location.reload(true)) socket.addEventListener('message', () => document.location.reload(true))
`, `,
}) })
} }

View File

@@ -17,7 +17,7 @@ const defaultOptions: Options = {
csl: "apa", csl: "apa",
} }
export const Citations: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { export const Citations: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "Citations", name: "Citations",
@@ -38,7 +38,7 @@ export const Citations: QuartzTransformerPlugin<Partial<Options> | undefined> =
// using https://github.com/syntax-tree/unist-util-visit as they're just anochor links // using https://github.com/syntax-tree/unist-util-visit as they're just anochor links
plugins.push(() => { plugins.push(() => {
return (tree, _file) => { return (tree, _file) => {
visit(tree, "element", (node, index, parent) => { visit(tree, "element", (node, _index, _parent) => {
if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) { if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) {
node.properties["data-no-popover"] = true node.properties["data-no-popover"] = true
} }

View File

@@ -18,7 +18,7 @@ const urlRegex = new RegExp(
"g", "g",
) )
export const Description: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { export const Description: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "Description", name: "Description",

View File

@@ -40,7 +40,7 @@ function coerceToArray(input: string | string[]): string[] | undefined {
.map((tag: string | number) => tag.toString()) .map((tag: string | number) => tag.toString())
} }
export const FrontMatter: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "FrontMatter", name: "FrontMatter",

View File

@@ -14,9 +14,7 @@ const defaultOptions: Options = {
linkHeadings: true, linkHeadings: true,
} }
export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = ( export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "GitHubFlavoredMarkdown", name: "GitHubFlavoredMarkdown",

View File

@@ -10,3 +10,4 @@ export { OxHugoFlavouredMarkdown } from "./oxhugofm"
export { SyntaxHighlighting } from "./syntax" export { SyntaxHighlighting } from "./syntax"
export { TableOfContents } from "./toc" export { TableOfContents } from "./toc"
export { HardLineBreaks } from "./linebreaks" export { HardLineBreaks } from "./linebreaks"
export { RoamFlavoredMarkdown } from "./roam"

View File

@@ -27,9 +27,7 @@ function coerceDate(fp: string, d: any): Date {
} }
type MaybeDate = undefined | string | number type MaybeDate = undefined | string | number
export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options> | undefined> = ( export const CreatedModifiedDate: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "CreatedModifiedDate", name: "CreatedModifiedDate",

View File

@@ -5,10 +5,16 @@ import { QuartzTransformerPlugin } from "../types"
interface Options { interface Options {
renderEngine: "katex" | "mathjax" renderEngine: "katex" | "mathjax"
customMacros: MacroType
} }
export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => { interface MacroType {
[key: string]: string
}
export const Latex: QuartzTransformerPlugin<Partial<Options>> = (opts) => {
const engine = opts?.renderEngine ?? "katex" const engine = opts?.renderEngine ?? "katex"
const macros = opts?.customMacros ?? {}
return { return {
name: "Latex", name: "Latex",
markdownPlugins() { markdownPlugins() {
@@ -16,9 +22,9 @@ export const Latex: QuartzTransformerPlugin<Options> = (opts?: Options) => {
}, },
htmlPlugins() { htmlPlugins() {
if (engine === "katex") { if (engine === "katex") {
return [[rehypeKatex, { output: "html" }]] return [[rehypeKatex, { output: "html", macros }]]
} else { } else {
return [rehypeMathjax] return [[rehypeMathjax, { macros }]]
} }
}, },
externalResources() { externalResources() {

View File

@@ -8,7 +8,6 @@ import {
simplifySlug, simplifySlug,
splitAnchor, splitAnchor,
transformLink, transformLink,
joinSegments,
} from "../../util/path" } from "../../util/path"
import path from "path" import path from "path"
import { visit } from "unist-util-visit" import { visit } from "unist-util-visit"
@@ -33,7 +32,7 @@ const defaultOptions: Options = {
externalLinkIcon: true, externalLinkIcon: true,
} }
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "LinkProcessing", name: "LinkProcessing",
@@ -66,7 +65,9 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
type: "element", type: "element",
tagName: "svg", tagName: "svg",
properties: { properties: {
"aria-hidden": "true",
class: "external-icon", class: "external-icon",
style: "max-width:0.8em;max-height:0.8em",
viewBox: "0 0 512 512", viewBox: "0 0 512 512",
}, },
children: [ children: [

View File

@@ -136,9 +136,7 @@ const wikilinkImageEmbedRegex = new RegExp(
/^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/, /^(?<alt>(?!^\d*x?\d*$).*?)?(\|?\s*?(?<width>\d+)(x(?<height>\d+))?)?$/,
) )
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = ( export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
const mdastToHtml = (ast: PhrasingContent | Paragraph) => { const mdastToHtml = (ast: PhrasingContent | Paragraph) => {
@@ -263,7 +261,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
} else if ([".pdf"].includes(ext)) { } else if ([".pdf"].includes(ext)) {
return { return {
type: "html", type: "html",
value: `<iframe src="${url}"></iframe>`, value: `<iframe src="${url}" class="pdf"></iframe>`,
} }
} else { } else {
const block = anchor const block = anchor
@@ -326,8 +324,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
replacements.push([ replacements.push([
tagRegex, tagRegex,
(_value: string, tag: string) => { (_value: string, tag: string) => {
// Check if the tag only includes numbers // Check if the tag only includes numbers and slashes
if (/^\d+$/.test(tag)) { if (/^[\/\d]+$/.test(tag)) {
return false return false
} }
@@ -616,11 +614,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
// YouTube video (with optional playlist) // YouTube video (with optional playlist)
node.tagName = "iframe" node.tagName = "iframe"
node.properties = { node.properties = {
class: "external-embed", class: "external-embed youtube",
allow: "fullscreen", allow: "fullscreen",
frameborder: 0, frameborder: 0,
width: "600px", width: "600px",
height: "350px",
src: playlistId src: playlistId
? `https://www.youtube.com/embed/${videoId}?list=${playlistId}` ? `https://www.youtube.com/embed/${videoId}?list=${playlistId}`
: `https://www.youtube.com/embed/${videoId}`, : `https://www.youtube.com/embed/${videoId}`,
@@ -629,11 +626,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
// YouTube playlist only. // YouTube playlist only.
node.tagName = "iframe" node.tagName = "iframe"
node.properties = { node.properties = {
class: "external-embed", class: "external-embed youtube",
allow: "fullscreen", allow: "fullscreen",
frameborder: 0, frameborder: 0,
width: "600px", width: "600px",
height: "350px",
src: `https://www.youtube.com/embed/videoseries?list=${playlistId}`, src: `https://www.youtube.com/embed/videoseries?list=${playlistId}`,
} }
} }

View File

@@ -47,9 +47,7 @@ const quartzLatexRegex = new RegExp(/\$\$[\s\S]*?\$\$|\$.*?\$/, "g")
* markdown to make it compatible with quartz but the list of changes applied it * markdown to make it compatible with quartz but the list of changes applied it
* is not exhaustive. * is not exhaustive.
* */ * */
export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = ( export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "OxHugoFlavouredMarkdown", name: "OxHugoFlavouredMarkdown",

View File

@@ -0,0 +1,224 @@
import { QuartzTransformerPlugin } from "../types"
import { PluggableList } from "unified"
import { SKIP, visit } from "unist-util-visit"
import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
import { Root, Html, Paragraph, Text, Link, Parent } from "mdast"
import { Node } from "unist"
import { VFile } from "vfile"
import { BuildVisitor } from "unist-util-visit"
export interface Options {
orComponent: boolean
TODOComponent: boolean
DONEComponent: boolean
videoComponent: boolean
audioComponent: boolean
pdfComponent: boolean
blockquoteComponent: boolean
tableComponent: boolean
attributeComponent: boolean
}
const defaultOptions: Options = {
orComponent: true,
TODOComponent: true,
DONEComponent: true,
videoComponent: true,
audioComponent: true,
pdfComponent: true,
blockquoteComponent: true,
tableComponent: true,
attributeComponent: true,
}
const orRegex = new RegExp(/{{or:(.*?)}}/, "g")
const TODORegex = new RegExp(/{{.*?\bTODO\b.*?}}/, "g")
const DONERegex = new RegExp(/{{.*?\bDONE\b.*?}}/, "g")
const videoRegex = new RegExp(/{{.*?\[\[video\]\].*?\:(.*?)}}/, "g")
const youtubeRegex = new RegExp(
/{{.*?\[\[video\]\].*?(https?:\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?[\w\?=]*)?)}}/,
"g",
)
// const multimediaRegex = new RegExp(/{{.*?\b(video|audio)\b.*?\:(.*?)}}/, "g")
const audioRegex = new RegExp(/{{.*?\[\[audio\]\].*?\:(.*?)}}/, "g")
const pdfRegex = new RegExp(/{{.*?\[\[pdf\]\].*?\:(.*?)}}/, "g")
const blockquoteRegex = new RegExp(/(\[\[>\]\])\s*(.*)/, "g")
const roamHighlightRegex = new RegExp(/\^\^(.+)\^\^/, "g")
const roamItalicRegex = new RegExp(/__(.+)__/, "g")
const tableRegex = new RegExp(/- {{.*?\btable\b.*?}}/, "g") /* TODO */
const attributeRegex = new RegExp(/\b\w+(?:\s+\w+)*::/, "g") /* TODO */
function isSpecialEmbed(node: Paragraph): boolean {
if (node.children.length !== 2) return false
const [textNode, linkNode] = node.children
return (
textNode.type === "text" &&
textNode.value.startsWith("{{[[") &&
linkNode.type === "link" &&
linkNode.children[0].type === "text" &&
linkNode.children[0].value.endsWith("}}")
)
}
function transformSpecialEmbed(node: Paragraph, opts: Options): Html | null {
const [textNode, linkNode] = node.children as [Text, Link]
const embedType = textNode.value.match(/\{\{\[\[(.*?)\]\]:/)?.[1]?.toLowerCase()
const url = linkNode.url.slice(0, -2) // Remove the trailing '}}'
switch (embedType) {
case "audio":
return opts.audioComponent
? {
type: "html",
value: `<audio controls>
<source src="${url}" type="audio/mpeg">
<source src="${url}" type="audio/ogg">
Your browser does not support the audio tag.
</audio>`,
}
: null
case "video":
if (!opts.videoComponent) return null
// Check if it's a YouTube video
const youtubeMatch = url.match(
/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=)?(.+)/,
)
if (youtubeMatch) {
const videoId = youtubeMatch[1].split("&")[0] // Remove additional parameters
const playlistMatch = url.match(/[?&]list=([^#\&\?]*)/)
const playlistId = playlistMatch ? playlistMatch[1] : null
return {
type: "html",
value: `<iframe
class="external-embed youtube"
width="600px"
height="350px"
src="https://www.youtube.com/embed/${videoId}${playlistId ? `?list=${playlistId}` : ""}"
frameborder="0"
allow="fullscreen"
></iframe>`,
}
} else {
return {
type: "html",
value: `<video controls>
<source src="${url}" type="video/mp4">
<source src="${url}" type="video/webm">
Your browser does not support the video tag.
</video>`,
}
}
case "pdf":
return opts.pdfComponent
? {
type: "html",
value: `<embed src="${url}" type="application/pdf" width="100%" height="600px" />`,
}
: null
default:
return null
}
}
export const RoamFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts }
return {
name: "RoamFlavoredMarkdown",
markdownPlugins() {
const plugins: PluggableList = []
plugins.push(() => {
return (tree: Root, file: VFile) => {
const replacements: [RegExp, ReplaceFunction][] = []
// Handle special embeds (audio, video, PDF)
if (opts.audioComponent || opts.videoComponent || opts.pdfComponent) {
visit(tree, "paragraph", ((node: Paragraph, index: number, parent: Parent | null) => {
if (isSpecialEmbed(node)) {
const transformedNode = transformSpecialEmbed(node, opts)
if (transformedNode && parent) {
parent.children[index] = transformedNode
}
}
}) as BuildVisitor<Root, "paragraph">)
}
// Roam italic syntax
replacements.push([
roamItalicRegex,
(_value: string, match: string) => ({
type: "emphasis",
children: [{ type: "text", value: match }],
}),
])
// Roam highlight syntax
replacements.push([
roamHighlightRegex,
(_value: string, inner: string) => ({
type: "html",
value: `<span class="text-highlight">${inner}</span>`,
}),
])
if (opts.orComponent) {
replacements.push([
orRegex,
(match: string) => {
const matchResult = match.match(/{{or:(.*?)}}/)
if (matchResult === null) {
return { type: "html", value: "" }
}
const optionsString: string = matchResult[1]
const options: string[] = optionsString.split("|")
const selectHtml: string = `<select>${options.map((option: string) => `<option value="${option}">${option}</option>`).join("")}</select>`
return { type: "html", value: selectHtml }
},
])
}
if (opts.TODOComponent) {
replacements.push([
TODORegex,
() => ({
type: "html",
value: `<input type="checkbox" disabled>`,
}),
])
}
if (opts.DONEComponent) {
replacements.push([
DONERegex,
() => ({
type: "html",
value: `<input type="checkbox" checked disabled>`,
}),
])
}
if (opts.blockquoteComponent) {
replacements.push([
blockquoteRegex,
(_match: string, _marker: string, content: string) => ({
type: "html",
value: `<blockquote>${content.trim()}</blockquote>`,
}),
])
}
mdastFindReplace(tree, replacements)
}
})
return plugins
},
}
}

View File

@@ -19,10 +19,8 @@ const defaultOptions: Options = {
keepBackground: false, keepBackground: false,
} }
export const SyntaxHighlighting: QuartzTransformerPlugin<Options> = ( export const SyntaxHighlighting: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
userOpts?: Partial<Options>, const opts: CodeOptions = { ...defaultOptions, ...userOpts }
) => {
const opts: Partial<CodeOptions> = { ...defaultOptions, ...userOpts }
return { return {
name: "SyntaxHighlighting", name: "SyntaxHighlighting",

View File

@@ -25,9 +25,7 @@ interface TocEntry {
} }
const slugAnchor = new Slugger() const slugAnchor = new Slugger()
export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefined> = ( export const TableOfContents: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts } const opts = { ...defaultOptions, ...userOpts }
return { return {
name: "TableOfContents", name: "TableOfContents",

View File

@@ -143,7 +143,7 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
const childPromises: WorkerPromise<ProcessedContent[]>[] = [] const childPromises: WorkerPromise<ProcessedContent[]>[] = []
for (const chunk of chunks(fps, CHUNK_SIZE)) { for (const chunk of chunks(fps, CHUNK_SIZE)) {
childPromises.push(pool.exec("parseFiles", [argv, chunk, ctx.allSlugs])) childPromises.push(pool.exec("parseFiles", [ctx.buildId, argv, chunk, ctx.allSlugs]))
} }
const results: ProcessedContent[][] = await WorkerPromise.all(childPromises).catch((err) => { const results: ProcessedContent[][] = await WorkerPromise.all(childPromises).catch((err) => {

View File

@@ -12,7 +12,6 @@ html {
body, body,
section { section {
margin: 0; margin: 0;
max-width: 100%;
box-sizing: border-box; box-sizing: border-box;
background-color: var(--light); background-color: var(--light);
font-family: var(--bodyFont); font-family: var(--bodyFont);
@@ -109,25 +108,21 @@ a {
.desktop-only { .desktop-only {
display: initial; display: initial;
@media all and (max-width: $fullPageWidth) { @media all and ($mobile) {
display: none; display: none;
} }
} }
.mobile-only { .mobile-only {
display: none; display: none;
@media all and (max-width: $fullPageWidth) { @media all and ($mobile) {
display: initial; display: initial;
} }
} }
.page { .page {
@media all and (max-width: $fullPageWidth) { max-width: calc(#{map-get($breakpoints, desktop)} + 300px);
margin: 0 auto; margin: 0 auto;
padding: 0 1rem;
max-width: $pageWidth;
}
& article { & article {
& > h1 { & > h1 {
font-size: 2rem; font-size: 2rem;
@@ -155,78 +150,117 @@ a {
} }
& > #quartz-body { & > #quartz-body {
width: 100%; display: grid;
display: flex; grid-template-columns: #{map-get($desktopGrid, templateColumns)};
@media all and (max-width: $fullPageWidth) { grid-template-rows: #{map-get($desktopGrid, templateRows)};
flex-direction: column; column-gap: #{map-get($desktopGrid, columnGap)};
row-gap: #{map-get($desktopGrid, rowGap)};
grid-template-areas: #{map-get($desktopGrid, templateAreas)};
@media all and ($desktop) {
grid-template-columns: #{map-get($tabletGrid, templateColumns)};
grid-template-rows: #{map-get($tabletGrid, templateRows)};
column-gap: #{map-get($tabletGrid, columnGap)};
row-gap: #{map-get($tabletGrid, rowGap)};
grid-template-areas: #{map-get($tabletGrid, templateAreas)};
}
@media all and ($mobile) {
grid-template-columns: #{map-get($mobileGrid, templateColumns)};
grid-template-rows: #{map-get($mobileGrid, templateRows)};
column-gap: #{map-get($mobileGrid, columnGap)};
row-gap: #{map-get($mobileGrid, rowGap)};
grid-template-areas: #{map-get($mobileGrid, templateAreas)};
}
@media all and ($desktop) {
padding: 0 1rem;
}
@media all and ($mobile) {
margin: 0 auto;
} }
& .sidebar { & .sidebar {
flex: 1;
display: flex;
flex-direction: column;
gap: 2rem; gap: 2rem;
top: 0; top: 0;
width: $sidePanelWidth;
margin-top: $topSpacing;
box-sizing: border-box; box-sizing: border-box;
padding: 0 4rem; padding: $topSpacing 2rem 2rem 2rem;
position: fixed; display: flex;
@media all and (max-width: $fullPageWidth) { height: 100vh;
position: initial; position: sticky;
flex-direction: row;
padding: 0;
width: initial;
margin-top: 2rem;
}
} }
& .sidebar.left { & .sidebar.left {
left: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth); z-index: 1;
@media all and (max-width: $fullPageWidth) { grid-area: sidebar-left;
flex-direction: column;
@media all and ($mobile) {
gap: 0; gap: 0;
align-items: center; align-items: center;
position: initial;
display: flex;
height: unset;
flex-direction: row;
padding: 0;
padding-top: 2rem;
} }
} }
& .sidebar.right { & .sidebar.right {
right: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth); grid-area: sidebar-right;
flex-wrap: wrap; margin-right: 0;
& > * { flex-direction: column;
@media all and (max-width: $fullPageWidth) { @media all and ($mobile) {
margin-left: inherit;
margin-right: inherit;
}
@media all and ($desktop) {
position: initial;
height: unset;
width: 100%;
flex-direction: row;
padding: 0;
& > * {
flex: 1; flex: 1;
min-width: 140px; }
& > .toc {
display: none;
} }
} }
} }
} & .page-header,
& .page-footer {
& .page-header, margin-top: 1rem;
& .page-footer {
width: $pageWidth;
margin-top: 1rem;
@media all and (max-width: $fullPageWidth) {
width: initial;
} }
}
& .page-header { & .page-header {
margin: $topSpacing auto 0 auto; grid-area: page-header;
@media all and (max-width: $fullPageWidth) { margin: $topSpacing 0 0 0;
margin-top: 2rem; @media all and ($mobile) {
margin-top: 0;
padding: 0;
}
} }
}
& .center, & .center > article {
& footer { grid-area: page-center;
margin-left: auto; }
margin-right: auto;
width: $pageWidth; & .page-footer {
@media all and (max-width: $fullPageWidth) { grid-area: page-footer;
width: initial; }
& .center,
& footer {
margin-left: auto;
margin-right: auto;
@media all and ($desktop) {
margin-right: 0;
}
@media all and ($mobile) {
margin-left: 0;
}
}
& footer {
margin-left: 0; margin-left: 0;
margin-right: 0;
} }
} }
} }
@@ -541,3 +575,11 @@ ol.overflow {
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
} }
.external-embed.youtube,
iframe.pdf {
aspect-ratio: 16 / 9;
height: 100%;
width: 100%;
border-radius: 5px;
}

View File

@@ -1,9 +1,56 @@
$pageWidth: 750px; /**
$mobileBreakpoint: 600px; * Layout breakpoints
$tabletBreakpoint: 1000px; * $mobile: screen width below this value will use mobile styles
$sidePanelWidth: 380px; * $desktop: screen width above this value will use desktop styles
* Screen width between $mobile and $desktop width will use the tablet layout.
* assuming mobile < desktop
*/
$breakpoints: (
mobile: 800px,
desktop: 1200px,
);
$mobile: "(max-width: #{map-get($breakpoints, mobile)})";
$tablet: "(min-width: #{map-get($breakpoints, mobile)}) and (max-width: #{map-get($breakpoints, desktop)})";
$desktop: "(max-width: #{map-get($breakpoints, desktop)})";
$pageWidth: #{map-get($breakpoints, mobile)};
$sidePanelWidth: 320px; //380px;
$topSpacing: 6rem; $topSpacing: 6rem;
$fullPageWidth: $pageWidth + 2 * $sidePanelWidth;
$boldWeight: 700; $boldWeight: 700;
$semiBoldWeight: 600; $semiBoldWeight: 600;
$normalWeight: 400; $normalWeight: 400;
$mobileGrid: (
templateRows: "auto auto auto auto auto",
templateColumns: "auto",
rowGap: "5px",
columnGap: "5px",
templateAreas:
'"sidebar-left"\
"page-header"\
"page-center"\
"sidebar-right"\
"page-footer"',
);
$tabletGrid: (
templateRows: "auto auto auto auto",
templateColumns: "#{$sidePanelWidth} auto",
rowGap: "5px",
columnGap: "5px",
templateAreas:
'"sidebar-left page-header"\
"sidebar-left page-center"\
"sidebar-left sidebar-right"\
"sidebar-left page-footer"',
);
$desktopGrid: (
templateRows: "auto auto auto",
templateColumns: "#{$sidePanelWidth} auto #{$sidePanelWidth}",
rowGap: "5px",
columnGap: "5px",
templateAreas:
'"sidebar-left page-header sidebar-right"\
"sidebar-left page-center sidebar-right"\
"sidebar-left page-footer sidebar-right"',
);

View File

@@ -14,6 +14,7 @@ export interface Argv {
} }
export interface BuildCtx { export interface BuildCtx {
buildId: string
argv: Argv argv: Argv
cfg: QuartzConfig cfg: QuartzConfig
allSlugs: FullSlug[] allSlugs: FullSlug[]

View File

@@ -7,8 +7,14 @@ import { createFileParser, createProcessor } from "./processors/parse"
import { options } from "./util/sourcemap" import { options } from "./util/sourcemap"
// only called from worker thread // only called from worker thread
export async function parseFiles(argv: Argv, fps: FilePath[], allSlugs: FullSlug[]) { export async function parseFiles(
buildId: string,
argv: Argv,
fps: FilePath[],
allSlugs: FullSlug[],
) {
const ctx: BuildCtx = { const ctx: BuildCtx = {
buildId,
cfg, cfg,
argv, argv,
allSlugs, allSlugs,