Compare commits

...

3 Commits

Author SHA1 Message Date
Jacky Zhao
2718ab9019 feat: flex component, document higher-order layout components
Some checks are pending
Build and Test / build-and-test (macos-latest) (push) Waiting to run
Build and Test / build-and-test (windows-latest) (push) Waiting to run
Build and Test / build-and-test (ubuntu-latest) (push) Has been skipped
Build and Test / publish-tag (push) Has been skipped
2025-03-11 14:56:43 -07:00
Jacky Zhao
87b803790c fix(mermaid): themechange detector + expand simplification 2025-03-11 11:45:45 -07:00
Jacky Zhao
e59181c3aa docs(explorer): update to account for new client side explorer) 2025-03-11 10:20:15 -07:00
11 changed files with 287 additions and 344 deletions

View File

@ -27,12 +27,10 @@ Component.Explorer({
folderClickBehavior: "collapse", // what happens when you click a folder ("link" to navigate to folder page on click or "collapse" to collapse folder on click) folderClickBehavior: "collapse", // what happens when you click a folder ("link" to navigate to folder page on click or "collapse" to collapse folder on click)
folderDefaultState: "collapsed", // default state of folders ("collapsed" or "open") folderDefaultState: "collapsed", // default state of folders ("collapsed" or "open")
useSavedState: true, // whether to use local storage to save "state" (which folders are opened) of explorer useSavedState: true, // whether to use local storage to save "state" (which folders are opened) of explorer
// Sort order: folders first, then files. Sort folders and files alphabetically // omitted but shown later
sortFn: (a, b) => { sortFn: ...,
... // default implementation shown later filterFn: ...,
}, mapFn: ...,
filterFn: filterFn: (node) => node.name !== "tags", // filters out 'tags' folder
mapFn: undefined,
// what order to apply functions in // what order to apply functions in
order: ["filter", "map", "sort"], order: ["filter", "map", "sort"],
}) })
@ -54,17 +52,23 @@ Want to customize it even more?
## Advanced customization ## Advanced customization
This component allows you to fully customize all of its behavior. You can pass a custom `sort`, `filter` and `map` function. This component allows you to fully customize all of its behavior. You can pass a custom `sort`, `filter` and `map` function.
All functions you can pass work with the `FileNode` class, which has the following properties: All functions you can pass work with the `FileTrieNode` class, which has the following properties:
```ts title="quartz/components/ExplorerNode.tsx" {2-5} ```ts title="quartz/components/Explorer.tsx"
export class FileNode { class FileTrieNode {
children: FileNode[] // children of current node isFolder: boolean
name: string // last part of slug children: Array<FileTrieNode>
displayName: string // what actually should be displayed in the explorer data: ContentDetails | null
file: QuartzPluginData | null // if node is a file, this is the file's metadata. See `QuartzPluginData` for more detail }
depth: number // depth of current node ```
... // rest of implementation ```ts title="quartz/plugins/emitters/contentIndex.tsx"
export type ContentDetails = {
slug: FullSlug
title: string
links: SimpleSlug[]
tags: string[]
content: string
} }
``` ```
@ -74,15 +78,14 @@ Every function you can pass is optional. By default, only a `sort` function will
// Sort order: folders first, then files. Sort folders and files alphabetically // Sort order: folders first, then files. Sort folders and files alphabetically
Component.Explorer({ Component.Explorer({
sortFn: (a, b) => { sortFn: (a, b) => {
if ((!a.file && !b.file) || (a.file && b.file)) { if ((!a.isFolder && !b.isFolder) || (a.isFolder && b.isFolder)) {
// sensitivity: "base": Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A
// numeric: true: Whether numeric collation should be used, such that "1" < "2" < "10"
return a.displayName.localeCompare(b.displayName, undefined, { return a.displayName.localeCompare(b.displayName, undefined, {
numeric: true, numeric: true,
sensitivity: "base", sensitivity: "base",
}) })
} }
if (a.file && !b.file) {
if (!a.isFolder && b.isFolder) {
return 1 return 1
} else { } else {
return -1 return -1
@ -100,41 +103,23 @@ For more information on how to use `sort`, `filter` and `map`, you can check [Ar
Type definitions look like this: Type definitions look like this:
```ts ```ts
sortFn: (a: FileNode, b: FileNode) => number type SortFn = (a: FileTrieNode, b: FileTrieNode) => number
filterFn: (node: FileNode) => boolean type FilterFn = (node: FileTrieNode) => boolean
mapFn: (node: FileNode) => void type MapFn = (node: FileTrieNode) => void
``` ```
> [!tip]
> You can check if a `FileNode` is a folder or a file like this:
>
> ```ts
> if (node.file) {
> // node is a file
> } else {
> // node is a folder
> }
> ```
## Basic examples ## Basic examples
These examples show the basic usage of `sort`, `map` and `filter`. These examples show the basic usage of `sort`, `map` and `filter`.
### Use `sort` to put files first ### Use `sort` to put files first
Using this example, the explorer will alphabetically sort everything, but put all **files** above all **folders**. Using this example, the explorer will alphabetically sort everything.
```ts title="quartz.layout.ts" ```ts title="quartz.layout.ts"
Component.Explorer({ Component.Explorer({
sortFn: (a, b) => { sortFn: (a, b) => {
if ((!a.file && !b.file) || (a.file && b.file)) {
return a.displayName.localeCompare(b.displayName) return a.displayName.localeCompare(b.displayName)
}
if (a.file && !b.file) {
return -1
} else {
return 1
}
}, },
}) })
``` ```
@ -146,43 +131,43 @@ Using this example, the display names of all `FileNodes` (folders + files) will
```ts title="quartz.layout.ts" ```ts title="quartz.layout.ts"
Component.Explorer({ Component.Explorer({
mapFn: (node) => { mapFn: (node) => {
node.displayName = node.displayName.toUpperCase() return (node.displayName = node.displayName.toUpperCase())
}, },
}) })
``` ```
### Remove list of elements (`filter`) ### Remove list of elements (`filter`)
Using this example, you can remove elements from your explorer by providing an array of folders/files using the `omit` set. Using this example, you can remove elements from your explorer by providing an array of folders/files to exclude.
Note that this example filters on the title but you can also do it via slug or any other field available on `FileTrieNode`.
```ts title="quartz.layout.ts" ```ts title="quartz.layout.ts"
Component.Explorer({ Component.Explorer({
filterFn: (node) => { filterFn: (node) => {
// set containing names of everything you want to filter out // set containing names of everything you want to filter out
const omit = new Set(["authoring content", "tags", "hosting"]) const omit = new Set(["authoring content", "tags", "hosting"])
return !omit.has(node.name.toLowerCase()) return !omit.has(node.data.title.toLowerCase())
}, },
}) })
``` ```
You can customize this by changing the entries of the `omit` set. Simply add all folder or file names you want to remove.
### Remove files by tag ### Remove files by tag
You can access the frontmatter of a file by `node.file?.frontmatter?`. This allows you to filter out files based on their frontmatter, for example by their tags. You can access the tags of a file by `node.data.tags`.
```ts title="quartz.layout.ts" ```ts title="quartz.layout.ts"
Component.Explorer({ Component.Explorer({
filterFn: (node) => { filterFn: (node) => {
// exclude files with the tag "explorerexclude" // exclude files with the tag "explorerexclude"
return node.file?.frontmatter?.tags?.includes("explorerexclude") !== true return node.data.tags.includes("explorerexclude") !== true
}, },
}) })
``` ```
### Show every element in explorer ### Show every element in explorer
To override the default filter function that removes the `tags` folder from the explorer, you can set the filter function to `undefined`. By default, the explorer will filter out the `tags` folder.
To override the default filter function, you can set the filter function to `undefined`.
```ts title="quartz.layout.ts" ```ts title="quartz.layout.ts"
Component.Explorer({ Component.Explorer({
@ -194,10 +179,12 @@ Component.Explorer({
> [!tip] > [!tip]
> When writing more complicated functions, the `layout` file can start to look very cramped. > When writing more complicated functions, the `layout` file can start to look very cramped.
> You can fix this by defining your functions in another file. > You can fix this by defining your sort functions outside of the component
> and passing it in.
> >
> ```ts title="functions.ts" > ```ts title="quartz.layout.ts"
> import { Options } from "./quartz/components/ExplorerNode" > import { Options } from "./quartz/components/ExplorerNode"
>
> export const mapFn: Options["mapFn"] = (node) => { > export const mapFn: Options["mapFn"] = (node) => {
> // implement your function here > // implement your function here
> } > }
@ -207,16 +194,12 @@ Component.Explorer({
> export const sortFn: Options["sortFn"] = (a, b) => { > export const sortFn: Options["sortFn"] = (a, b) => {
> // implement your function here > // implement your function here
> } > }
> ```
> >
> You can then import them like this:
>
> ```ts title="quartz.layout.ts"
> import { mapFn, filterFn, sortFn } from "./functions.ts"
> Component.Explorer({ > Component.Explorer({
> mapFn: mapFn, > // ... your other options
> filterFn: filterFn, > mapFn,
> sortFn: sortFn, > filterFn,
> sortFn,
> }) > })
> ``` > ```
@ -227,93 +210,11 @@ To add emoji prefixes (📁 for folders, 📄 for files), you could use a map fu
```ts title="quartz.layout.ts" ```ts title="quartz.layout.ts"
Component.Explorer({ Component.Explorer({
mapFn: (node) => { mapFn: (node) => {
// dont change name of root node if (node.isFolder) {
if (node.depth > 0) {
// set emoji for file/folder
if (node.file) {
node.displayName = "📄 " + node.displayName
} else {
node.displayName = "📁 " + node.displayName node.displayName = "📁 " + node.displayName
} } else {
node.displayName = "📄 " + node.displayName
} }
}, },
}) })
``` ```
### Putting it all together
In this example, we're going to customize the explorer by using functions from examples above to [[#Add emoji prefix | add emoji prefixes]], [[#remove-list-of-elements-filter| filter out some folders]] and [[#use-sort-to-put-files-first | sort with files above folders]].
```ts title="quartz.layout.ts"
Component.Explorer({
filterFn: sampleFilterFn,
mapFn: sampleMapFn,
sortFn: sampleSortFn,
order: ["filter", "sort", "map"],
})
```
Notice how we customized the `order` array here. This is done because the default order applies the `sort` function last. While this normally works well, it would cause unintended behavior here, since we changed the first characters of all display names. In our example, `sort` would be applied based off the emoji prefix instead of the first _real_ character.
To fix this, we just changed around the order and apply the `sort` function before changing the display names in the `map` function.
### Use `sort` with pre-defined sort order
Here's another example where a map containing file/folder names (as slugs) is used to define the sort order of the explorer in quartz. All files/folders that aren't listed inside of `nameOrderMap` will appear at the top of that folders hierarchy level.
It's also worth mentioning, that the smaller the number set in `nameOrderMap`, the higher up the entry will be in the explorer. Incrementing every folder/file by 100, makes ordering files in their folders a lot easier. Lastly, this example still allows you to use a `mapFn` or frontmatter titles to change display names, as it uses slugs for `nameOrderMap` (which is unaffected by display name changes).
```ts title="quartz.layout.ts"
Component.Explorer({
sortFn: (a, b) => {
const nameOrderMap: Record<string, number> = {
"poetry-folder": 100,
"essay-folder": 200,
"research-paper-file": 201,
"dinosaur-fossils-file": 300,
"other-folder": 400,
}
let orderA = 0
let orderB = 0
if (a.file && a.file.slug) {
orderA = nameOrderMap[a.file.slug] || 0
} else if (a.name) {
orderA = nameOrderMap[a.name] || 0
}
if (b.file && b.file.slug) {
orderB = nameOrderMap[b.file.slug] || 0
} else if (b.name) {
orderB = nameOrderMap[b.name] || 0
}
return orderA - orderB
},
})
```
For reference, this is how the quartz explorer window would look like with that example:
```
📖 Poetry Folder
📑 Essay Folder
⚗️ Research Paper File
🦴 Dinosaur Fossils File
🔮 Other Folder
```
And this is how the file structure would look like:
```
index.md
poetry-folder
index.md
essay-folder
index.md
research-paper-file.md
dinosaur-fossils-file.md
other-folder
index.md
```

62
docs/layout-components.md Normal file
View File

@ -0,0 +1,62 @@
---
title: Higher-Order Layout Components
---
Quartz provides several higher-order components that help with layout composition and responsive design. These components wrap other components to add additional functionality or modify their behavior.
## `Flex` Component
The `Flex` component creates a [flexible box layout](https://developer.mozilla.org/en-US/docs/Web/CSS/flex) that can arrange child components in various ways. It's particularly useful for creating responsive layouts and organizing components in rows or columns.
```typescript
type FlexConfig = {
components: {
Component: QuartzComponent
grow?: boolean // whether component should grow to fill space
shrink?: boolean // whether component should shrink if needed
basis?: string // initial main size of the component
order?: number // order in flex container
align?: "start" | "end" | "center" | "stretch" // cross-axis alignment
justify?: "start" | "end" | "center" | "between" | "around" // main-axis alignment
}[]
direction?: "row" | "row-reverse" | "column" | "column-reverse"
wrap?: "nowrap" | "wrap" | "wrap-reverse"
gap?: string
}
```
### Example Usage
```typescript
Component.Flex({
components: [
{
Component: Component.Search(),
grow: true, // Search will grow to fill available space
},
{ Component: Component.Darkmode() }, // Darkmode keeps its natural size
],
direction: "row",
gap: "1rem",
})
```
## `MobileOnly` Component
The `MobileOnly` component is a wrapper that makes its child component only visible on mobile devices. This is useful for creating responsive layouts where certain components should only appear on smaller screens.
### Example Usage
```typescript
Component.MobileOnly(Component.Spacer())
```
## `DesktopOnly` Component
The `DesktopOnly` component is the counterpart to `MobileOnly`. It makes its child component only visible on desktop devices. This helps create responsive layouts where certain components should only appear on larger screens.
### Example Usage
```typescript
Component.DesktopOnly(Component.TableOfContents())
```

View File

@ -35,7 +35,9 @@ These correspond to following parts of the page:
Quartz **components**, like plugins, can take in additional properties as configuration options. If you're familiar with React terminology, you can think of them as Higher-order Components. Quartz **components**, like plugins, can take in additional properties as configuration options. If you're familiar with React terminology, you can think of them as Higher-order Components.
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. Additionally, Quartz provides several built-in higher-order components for layout composition - see [[layout-components]] for more details.
You can also checkout the guide on [[creating components]] if you're interested in further customizing the behaviour of Quartz.
### Layout breakpoints ### Layout breakpoints

View File

@ -25,8 +25,15 @@ export const defaultContentPageLayout: PageLayout = {
left: [ left: [
Component.PageTitle(), Component.PageTitle(),
Component.MobileOnly(Component.Spacer()), Component.MobileOnly(Component.Spacer()),
Component.Search(), Component.Flex({
Component.Darkmode(), components: [
{
Component: Component.Search(),
grow: true,
},
{ Component: Component.Darkmode() },
],
}),
Component.Explorer(), Component.Explorer(),
], ],
right: [ right: [

View File

@ -1,7 +1,6 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
export default ((component?: QuartzComponent) => { export default ((component: QuartzComponent) => {
if (component) {
const Component = component const Component = component
const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => { const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => {
return <Component displayClass="desktop-only" {...props} /> return <Component displayClass="desktop-only" {...props} />
@ -12,7 +11,4 @@ export default ((component?: QuartzComponent) => {
DesktopOnly.beforeDOMLoaded = component?.beforeDOMLoaded DesktopOnly.beforeDOMLoaded = component?.beforeDOMLoaded
DesktopOnly.css = component?.css DesktopOnly.css = component?.css
return DesktopOnly return DesktopOnly
} else { }) satisfies QuartzComponentConstructor<QuartzComponent>
return () => <></>
}
}) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,55 @@
import { concatenateResources } from "../util/resources"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
type FlexConfig = {
components: {
Component: QuartzComponent
grow?: boolean
shrink?: boolean
basis?: string
order?: number
align?: "start" | "end" | "center" | "stretch"
justify?: "start" | "end" | "center" | "between" | "around"
}[]
direction?: "row" | "row-reverse" | "column" | "column-reverse"
wrap?: "nowrap" | "wrap" | "wrap-reverse"
gap?: string
}
export default ((config: FlexConfig) => {
const Flex: QuartzComponent = (props: QuartzComponentProps) => {
const direction = config.direction ?? "row"
const wrap = config.wrap ?? "nowrap"
const gap = config.gap ?? "1rem"
return (
<div style={`display: flex; flex-direction: ${direction}; flex-wrap: ${wrap}; gap: ${gap};`}>
{config.components.map((c) => {
const grow = c.grow ? 1 : 0
const shrink = (c.shrink ?? true) ? 1 : 0
const basis = c.basis ?? "auto"
const order = c.order ?? 0
const align = c.align ?? "center"
const justify = c.justify ?? "center"
return (
<div
style={`flex-grow: ${grow}; flex-shrink: ${shrink}; flex-basis: ${basis}; order: ${order}; align-self: ${align}; justify-self: ${justify};`}
>
<c.Component {...props} />
</div>
)
})}
</div>
)
}
Flex.afterDOMLoaded = concatenateResources(
...config.components.map((c) => c.Component.afterDOMLoaded),
)
Flex.beforeDOMLoaded = concatenateResources(
...config.components.map((c) => c.Component.beforeDOMLoaded),
)
Flex.css = concatenateResources(...config.components.map((c) => c.Component.css))
return Flex
}) satisfies QuartzComponentConstructor<FlexConfig>

View File

@ -1,7 +1,6 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
export default ((component?: QuartzComponent) => { export default ((component: QuartzComponent) => {
if (component) {
const Component = component const Component = component
const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => { const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => {
return <Component displayClass="mobile-only" {...props} /> return <Component displayClass="mobile-only" {...props} />
@ -12,7 +11,4 @@ export default ((component?: QuartzComponent) => {
MobileOnly.beforeDOMLoaded = component?.beforeDOMLoaded MobileOnly.beforeDOMLoaded = component?.beforeDOMLoaded
MobileOnly.css = component?.css MobileOnly.css = component?.css
return MobileOnly return MobileOnly
} else { }) satisfies QuartzComponentConstructor<QuartzComponent>
return () => <></>
}
}) satisfies QuartzComponentConstructor

View File

@ -20,6 +20,7 @@ import MobileOnly from "./MobileOnly"
import RecentNotes from "./RecentNotes" import RecentNotes from "./RecentNotes"
import Breadcrumbs from "./Breadcrumbs" import Breadcrumbs from "./Breadcrumbs"
import Comments from "./Comments" import Comments from "./Comments"
import Flex from "./Flex"
export { export {
ArticleTitle, ArticleTitle,
@ -44,4 +45,5 @@ export {
NotFound, NotFound,
Breadcrumbs, Breadcrumbs,
Comments, Comments,
Flex,
} }

View File

@ -12,7 +12,8 @@ class DiagramPanZoom {
private scale = 1 private scale = 1
private readonly MIN_SCALE = 0.5 private readonly MIN_SCALE = 0.5
private readonly MAX_SCALE = 3 private readonly MAX_SCALE = 3
private readonly ZOOM_SENSITIVITY = 0.001
cleanups: (() => void)[] = []
constructor( constructor(
private container: HTMLElement, private container: HTMLElement,
@ -20,19 +21,33 @@ class DiagramPanZoom {
) { ) {
this.setupEventListeners() this.setupEventListeners()
this.setupNavigationControls() this.setupNavigationControls()
this.resetTransform()
} }
private setupEventListeners() { private setupEventListeners() {
// Mouse drag events // Mouse drag events
this.container.addEventListener("mousedown", this.onMouseDown.bind(this)) const mouseDownHandler = this.onMouseDown.bind(this)
document.addEventListener("mousemove", this.onMouseMove.bind(this)) const mouseMoveHandler = this.onMouseMove.bind(this)
document.addEventListener("mouseup", this.onMouseUp.bind(this)) const mouseUpHandler = this.onMouseUp.bind(this)
const resizeHandler = this.resetTransform.bind(this)
// Wheel zoom events this.container.addEventListener("mousedown", mouseDownHandler)
this.container.addEventListener("wheel", this.onWheel.bind(this), { passive: false }) document.addEventListener("mousemove", mouseMoveHandler)
document.addEventListener("mouseup", mouseUpHandler)
window.addEventListener("resize", resizeHandler)
// Reset on window resize this.cleanups.push(
window.addEventListener("resize", this.resetTransform.bind(this)) () => this.container.removeEventListener("mousedown", mouseDownHandler),
() => document.removeEventListener("mousemove", mouseMoveHandler),
() => document.removeEventListener("mouseup", mouseUpHandler),
() => window.removeEventListener("resize", resizeHandler),
)
}
cleanup() {
for (const cleanup of this.cleanups) {
cleanup()
}
} }
private setupNavigationControls() { private setupNavigationControls() {
@ -84,26 +99,6 @@ class DiagramPanZoom {
this.container.style.cursor = "grab" this.container.style.cursor = "grab"
} }
private onWheel(e: WheelEvent) {
e.preventDefault()
const delta = -e.deltaY * this.ZOOM_SENSITIVITY
const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE)
// Calculate mouse position relative to content
const rect = this.content.getBoundingClientRect()
const mouseX = e.clientX - rect.left
const mouseY = e.clientY - rect.top
// Adjust pan to zoom around mouse position
const scaleDiff = newScale - this.scale
this.currentPan.x -= mouseX * scaleDiff
this.currentPan.y -= mouseY * scaleDiff
this.scale = newScale
this.updateTransform()
}
private zoom(delta: number) { private zoom(delta: number) {
const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE) const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE)
@ -126,7 +121,11 @@ class DiagramPanZoom {
private resetTransform() { private resetTransform() {
this.scale = 1 this.scale = 1
this.currentPan = { x: 0, y: 0 } const svg = this.content.querySelector("svg")!
this.currentPan = {
x: svg.getBoundingClientRect().width / 2,
y: svg.getBoundingClientRect().height / 2,
}
this.updateTransform() this.updateTransform()
} }
} }
@ -149,20 +148,35 @@ document.addEventListener("nav", async () => {
const nodes = center.querySelectorAll("code.mermaid") as NodeListOf<HTMLElement> const nodes = center.querySelectorAll("code.mermaid") as NodeListOf<HTMLElement>
if (nodes.length === 0) return if (nodes.length === 0) return
const computedStyleMap = cssVars.reduce(
(acc, key) => {
acc[key] = getComputedStyle(document.documentElement).getPropertyValue(key)
return acc
},
{} as Record<(typeof cssVars)[number], string>,
)
mermaidImport ||= await import( mermaidImport ||= await import(
// @ts-ignore // @ts-ignore
"https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.4.0/mermaid.esm.min.mjs" "https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.4.0/mermaid.esm.min.mjs"
) )
const mermaid = mermaidImport.default const mermaid = mermaidImport.default
const textMapping: WeakMap<HTMLElement, string> = new WeakMap()
for (const node of nodes) {
textMapping.set(node, node.innerText)
}
async function renderMermaid() {
// de-init any other diagrams
for (const node of nodes) {
node.removeAttribute("data-processed")
const oldText = textMapping.get(node)
if (oldText) {
node.innerHTML = oldText
}
}
const computedStyleMap = cssVars.reduce(
(acc, key) => {
acc[key] = window.getComputedStyle(document.documentElement).getPropertyValue(key)
return acc
},
{} as Record<(typeof cssVars)[number], string>,
)
const darkMode = document.documentElement.getAttribute("saved-theme") === "dark" const darkMode = document.documentElement.getAttribute("saved-theme") === "dark"
mermaid.initialize({ mermaid.initialize({
startOnLoad: false, startOnLoad: false,
@ -180,7 +194,13 @@ document.addEventListener("nav", async () => {
edgeLabelBackground: computedStyleMap["--highlight"], edgeLabelBackground: computedStyleMap["--highlight"],
}, },
}) })
await mermaid.run({ nodes }) await mermaid.run({ nodes })
}
await renderMermaid()
document.addEventListener("themechange", renderMermaid)
window.addCleanup(() => document.removeEventListener("themechange", renderMermaid))
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
const codeBlock = nodes[i] as HTMLElement const codeBlock = nodes[i] as HTMLElement
@ -203,7 +223,6 @@ document.addEventListener("nav", async () => {
if (!popupContainer) return if (!popupContainer) return
let panZoom: DiagramPanZoom | null = null let panZoom: DiagramPanZoom | null = null
function showMermaid() { function showMermaid() {
const container = popupContainer.querySelector("#mermaid-space") as HTMLElement const container = popupContainer.querySelector("#mermaid-space") as HTMLElement
const content = popupContainer.querySelector(".mermaid-content") as HTMLElement const content = popupContainer.querySelector(".mermaid-content") as HTMLElement
@ -224,24 +243,15 @@ document.addEventListener("nav", async () => {
function hideMermaid() { function hideMermaid() {
popupContainer.classList.remove("active") popupContainer.classList.remove("active")
panZoom?.cleanup()
panZoom = null panZoom = null
} }
function handleEscape(e: any) {
if (e.key === "Escape") {
hideMermaid()
}
}
const closeBtn = popupContainer.querySelector(".close-button") as HTMLButtonElement
closeBtn.addEventListener("click", hideMermaid)
expandBtn.addEventListener("click", showMermaid) expandBtn.addEventListener("click", showMermaid)
registerEscapeHandler(popupContainer, hideMermaid) registerEscapeHandler(popupContainer, hideMermaid)
document.addEventListener("keydown", handleEscape)
window.addCleanup(() => { window.addCleanup(() => {
closeBtn.removeEventListener("click", hideMermaid) panZoom?.cleanup()
expandBtn.removeEventListener("click", showMermaid) expandBtn.removeEventListener("click", showMermaid)
}) })
} }

View File

@ -53,46 +53,16 @@ pre {
} }
& > #mermaid-space { & > #mermaid-space {
display: grid; border: 1px solid var(--lightgray);
width: 90%; background-color: var(--light);
height: 90vh; border-radius: 5px;
margin: 5vh auto; position: fixed;
background: var(--light); top: 50%;
box-shadow: left: 50%;
0 14px 50px rgba(27, 33, 48, 0.12), transform: translate(-50%, -50%);
0 10px 30px rgba(27, 33, 48, 0.16); height: 80vh;
width: 80vw;
overflow: hidden; overflow: hidden;
position: relative;
& > .mermaid-header {
display: flex;
justify-content: flex-end;
padding: 1rem;
border-bottom: 1px solid var(--lightgray);
background: var(--light);
z-index: 2;
max-height: fit-content;
& > .close-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
background: transparent;
border: none;
border-radius: 4px;
color: var(--darkgray);
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: var(--lightgray);
color: var(--dark);
}
}
}
& > .mermaid-content { & > .mermaid-content {
padding: 2rem; padding: 2rem;

View File

@ -675,7 +675,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
properties: { properties: {
className: ["expand-button"], className: ["expand-button"],
"aria-label": "Expand mermaid diagram", "aria-label": "Expand mermaid diagram",
"aria-hidden": "true",
"data-view-component": true, "data-view-component": true,
}, },
children: [ children: [
@ -706,70 +705,13 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
{ {
type: "element", type: "element",
tagName: "div", tagName: "div",
properties: { id: "mermaid-container" }, properties: { id: "mermaid-container", role: "dialog" },
children: [ children: [
{ {
type: "element", type: "element",
tagName: "div", tagName: "div",
properties: { id: "mermaid-space" }, properties: { id: "mermaid-space" },
children: [ children: [
{
type: "element",
tagName: "div",
properties: { className: ["mermaid-header"] },
children: [
{
type: "element",
tagName: "button",
properties: {
className: ["close-button"],
"aria-label": "close button",
},
children: [
{
type: "element",
tagName: "svg",
properties: {
"aria-hidden": "true",
xmlns: "http://www.w3.org/2000/svg",
width: 24,
height: 24,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
},
children: [
{
type: "element",
tagName: "line",
properties: {
x1: 18,
y1: 6,
x2: 6,
y2: 18,
},
children: [],
},
{
type: "element",
tagName: "line",
properties: {
x1: 6,
y1: 6,
x2: 18,
y2: 18,
},
children: [],
},
],
},
],
},
],
},
{ {
type: "element", type: "element",
tagName: "div", tagName: "div",