mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-05-18 22:44:14 +02:00
Compare commits
3 Commits
b00198b888
...
2718ab9019
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2718ab9019 | ||
![]() |
87b803790c | ||
![]() |
e59181c3aa |
@ -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
62
docs/layout-components.md
Normal 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())
|
||||||
|
```
|
@ -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
|
||||||
|
|
||||||
|
@ -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: [
|
||||||
|
@ -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
|
|
||||||
|
55
quartz/components/Flex.tsx
Normal file
55
quartz/components/Flex.tsx
Normal 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>
|
@ -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
|
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user