feat: integrate CanvasPage plugin with types, assets, config, layout, and documentation

This commit is contained in:
saberzero1 2026-02-14 15:36:15 +01:00
parent 616ae5450e
commit 1538e844ae
No known key found for this signature in database
10 changed files with 513 additions and 12 deletions

View File

@ -0,0 +1,81 @@
{
"nodes": [
{
"id": "node-1",
"type": "text",
"x": 0,
"y": 0,
"width": 250,
"height": 120,
"text": "# Welcome\nThis is a **text node** with markdown content.",
"color": "1"
},
{
"id": "node-2",
"type": "text",
"x": 400,
"y": -50,
"width": 200,
"height": 100,
"text": "A second text node connected to the first."
},
{
"id": "node-3",
"type": "file",
"x": 0,
"y": 200,
"width": 250,
"height": 80,
"file": "index.md",
"color": "4"
},
{
"id": "node-4",
"type": "link",
"x": 400,
"y": 200,
"width": 250,
"height": 80,
"url": "https://jsoncanvas.org",
"color": "5"
},
{
"id": "group-1",
"type": "group",
"x": -30,
"y": -80,
"width": 700,
"height": 200,
"label": "Main Ideas",
"color": "6"
}
],
"edges": [
{
"id": "edge-1",
"fromNode": "node-1",
"fromSide": "right",
"toNode": "node-2",
"toSide": "left",
"toEnd": "arrow",
"label": "connects to"
},
{
"id": "edge-2",
"fromNode": "node-1",
"fromSide": "bottom",
"toNode": "node-3",
"toSide": "top",
"toEnd": "arrow"
},
{
"id": "edge-3",
"fromNode": "node-2",
"fromSide": "bottom",
"toNode": "node-4",
"toSide": "top",
"toEnd": "arrow",
"color": "#ff6600"
}
]
}

7
content/index.md Normal file
View File

@ -0,0 +1,7 @@
---
title: Welcome
---
This is a test site for Quartz v5.
- [[example-canvas|Example Canvas]]

View File

@ -0,0 +1,330 @@
{
"nodes": [
{
"id": "title",
"type": "text",
"x": 0,
"y": 0,
"width": 560,
"height": 200,
"text": "# CanvasPage Plugin\n\nThis plugin renders [JSON Canvas](https://jsoncanvas.org) (`.canvas`) files as interactive, pannable and zoomable canvas pages. It supports the full [JSON Canvas 1.0 spec](https://jsoncanvas.org/spec/1.0/).\n\nInstall: `npx quartz plugin add github:quartz-community/canvas-page`",
"color": "5"
},
{
"id": "group-node-types",
"type": "group",
"x": -30,
"y": 260,
"width": 1220,
"height": 460,
"label": "Node Types",
"color": "6"
},
{
"id": "text-node-demo",
"type": "text",
"x": 0,
"y": 300,
"width": 360,
"height": 280,
"text": "## Text Nodes\n\nText nodes render **Markdown** content with GFM support:\n\n- **Bold** and *italic* text\n- ~~Strikethrough~~ text\n- [External links](https://jsoncanvas.org)\n- `Inline code` blocks\n- Lists (like this one)\n\n### Headings Work Too\n\nAll standard Markdown syntax is rendered at build time.",
"color": "1"
},
{
"id": "file-node-info",
"type": "text",
"x": 400,
"y": 300,
"width": 360,
"height": 160,
"text": "## File Nodes\n\nFile nodes reference other pages in your vault. They appear as clickable links and support **popover previews** on hover.\n\nThe node below links to the CanvasPage documentation:",
"color": "2"
},
{
"id": "file-node-demo",
"type": "file",
"x": 400,
"y": 500,
"width": 360,
"height": 80,
"file": "docs/plugins/CanvasPage.md",
"color": "4"
},
{
"id": "link-node-info",
"type": "text",
"x": 800,
"y": 300,
"width": 360,
"height": 120,
"text": "## Link Nodes\n\nLink nodes reference external URLs. The node below links to the JSON Canvas specification:",
"color": "3"
},
{
"id": "link-node-demo",
"type": "link",
"x": 800,
"y": 460,
"width": 360,
"height": 80,
"url": "https://jsoncanvas.org/spec/1.0/",
"color": "5"
},
{
"id": "group-colors",
"type": "group",
"x": -30,
"y": 790,
"width": 1220,
"height": 320,
"label": "Preset Colors",
"color": "4"
},
{
"id": "color-1",
"type": "text",
"x": 0,
"y": 830,
"width": 180,
"height": 80,
"text": "**Color 1** — Red",
"color": "1"
},
{
"id": "color-2",
"type": "text",
"x": 200,
"y": 830,
"width": 180,
"height": 80,
"text": "**Color 2** — Orange",
"color": "2"
},
{
"id": "color-3",
"type": "text",
"x": 400,
"y": 830,
"width": 180,
"height": 80,
"text": "**Color 3** — Yellow",
"color": "3"
},
{
"id": "color-4",
"type": "text",
"x": 600,
"y": 830,
"width": 180,
"height": 80,
"text": "**Color 4** — Green",
"color": "4"
},
{
"id": "color-5",
"type": "text",
"x": 800,
"y": 830,
"width": 180,
"height": 80,
"text": "**Color 5** — Cyan",
"color": "5"
},
{
"id": "color-6",
"type": "text",
"x": 1000,
"y": 830,
"width": 180,
"height": 80,
"text": "**Color 6** — Purple",
"color": "6"
},
{
"id": "color-custom",
"type": "text",
"x": 400,
"y": 950,
"width": 380,
"height": 80,
"text": "**Custom hex color** — `#ff6600`",
"color": "#ff6600"
},
{
"id": "group-config",
"type": "group",
"x": -30,
"y": 1180,
"width": 1220,
"height": 360,
"label": "Configuration",
"color": "2"
},
{
"id": "config-options",
"type": "text",
"x": 0,
"y": 1220,
"width": 560,
"height": 280,
"text": "## Configuration Options\n\n- `enableInteraction` — Enable pan and zoom. Default: `true`\n- `initialZoom` — Initial zoom level. Default: `1`\n- `minZoom` — Minimum zoom level. Default: `0.1`\n- `maxZoom` — Maximum zoom level. Default: `5`\n- `defaultFullscreen` — Start in fullscreen mode. Default: `false`\n\nConfigure in `quartz.config.ts`:\n\n```\nCanvasPage({ defaultFullscreen: false, initialZoom: 1 })\n```"
},
{
"id": "config-fullscreen",
"type": "text",
"x": 600,
"y": 1220,
"width": 560,
"height": 280,
"text": "## Fullscreen Mode\n\nClick the **expand button** (top-right corner) to toggle fullscreen mode. The canvas fills the entire viewport.\n\n- Press **Escape** to exit fullscreen\n- Set `defaultFullscreen: true` to start in fullscreen\n- The toggle button switches between expand and collapse icons\n\n## Quartz Integration\n\n- **Popover previews**: Hover over file nodes to see a preview\n- **Internal links**: File nodes link to pages in your vault\n- **Dark mode**: Canvas adapts to your theme settings"
},
{
"id": "group-edges",
"type": "group",
"x": -30,
"y": 1610,
"width": 1220,
"height": 320,
"label": "Edges & Connections",
"color": "3"
},
{
"id": "edge-source",
"type": "text",
"x": 0,
"y": 1650,
"width": 300,
"height": 120,
"text": "## Edges\n\nEdges connect nodes with SVG paths. They support **labels**, **arrows**, and **colors**.",
"color": "1"
},
{
"id": "edge-labeled",
"type": "text",
"x": 450,
"y": 1650,
"width": 260,
"height": 80,
"text": "This edge has a **label** and an arrow marker.",
"color": "4"
},
{
"id": "edge-colored",
"type": "text",
"x": 450,
"y": 1780,
"width": 260,
"height": 80,
"text": "This edge has a **custom color** (`#ff6600`).",
"color": "2"
},
{
"id": "edge-preset",
"type": "text",
"x": 850,
"y": 1650,
"width": 300,
"height": 120,
"text": "Edges can use the same **preset colors** (16) as nodes, or custom **hex colors** like `#ff6600`.",
"color": "6"
},
{
"id": "api-info",
"type": "text",
"x": 0,
"y": 2000,
"width": 560,
"height": 180,
"text": "## API\n\n- **Category**: Page Type\n- **Function name**: `ExternalPlugin.CanvasPage()`\n- **Source**: [quartz-community/canvas-page](https://github.com/quartz-community/canvas-page)\n- **Install**: `npx quartz plugin add github:quartz-community/canvas-page`"
},
{
"id": "spec-info",
"type": "text",
"x": 600,
"y": 2000,
"width": 560,
"height": 180,
"text": "## JSON Canvas Spec\n\nThis plugin implements the [JSON Canvas 1.0](https://jsoncanvas.org/spec/1.0/) specification — an open file format for infinite canvas data.\n\nCanvas files use the `.canvas` extension and are standard JSON. They are natively supported by [Obsidian](https://obsidian.md)."
}
],
"edges": [
{
"id": "edge-title-to-types",
"fromNode": "title",
"fromSide": "bottom",
"toNode": "group-node-types",
"toSide": "top",
"toEnd": "arrow",
"label": "supports"
},
{
"id": "edge-info-to-file",
"fromNode": "file-node-info",
"fromSide": "bottom",
"toNode": "file-node-demo",
"toSide": "top",
"toEnd": "arrow",
"color": "4"
},
{
"id": "edge-info-to-link",
"fromNode": "link-node-info",
"fromSide": "bottom",
"toNode": "link-node-demo",
"toSide": "top",
"toEnd": "arrow",
"color": "5"
},
{
"id": "edge-types-to-colors",
"fromNode": "group-node-types",
"fromSide": "bottom",
"toNode": "group-colors",
"toSide": "top",
"toEnd": "arrow"
},
{
"id": "edge-colors-to-config",
"fromNode": "group-colors",
"fromSide": "bottom",
"toNode": "group-config",
"toSide": "top",
"toEnd": "arrow"
},
{
"id": "edge-config-to-edges",
"fromNode": "group-config",
"fromSide": "bottom",
"toNode": "group-edges",
"toSide": "top",
"toEnd": "arrow"
},
{
"id": "edge-labeled-demo",
"fromNode": "edge-source",
"fromSide": "right",
"toNode": "edge-labeled",
"toSide": "left",
"toEnd": "arrow",
"label": "labeled edge"
},
{
"id": "edge-colored-demo",
"fromNode": "edge-source",
"fromSide": "right",
"toNode": "edge-colored",
"toSide": "left",
"toEnd": "arrow",
"color": "#ff6600"
},
{
"id": "edge-preset-demo",
"fromNode": "edge-labeled",
"fromSide": "right",
"toNode": "edge-preset",
"toSide": "left",
"toEnd": "arrow",
"color": "6"
}
]
}

View File

@ -0,0 +1,37 @@
---
title: CanvasPage
tags:
- plugin/pageType
---
This plugin is a page type plugin that renders [JSON Canvas](https://jsoncanvas.org) (`.canvas`) files as interactive, pannable and zoomable canvas pages. It supports the full [JSON Canvas 1.0 spec](https://jsoncanvas.org/spec/1.0/), including text nodes with Markdown rendering, file nodes that link to other pages in your vault, link nodes for external URLs, and group nodes for visual organization. Edges between nodes are rendered as SVG paths with optional labels, arrow markers, and colors.
See [[CanvasPage.canvas]] for a live demo showcasing all supported features.
> [!note]
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
This plugin accepts the following configuration options:
- `enableInteraction`: Whether to enable pan and zoom interaction on the canvas. Default: `true{:ts}`.
- `initialZoom`: The initial zoom level when the canvas is first displayed. Default: `1{:ts}`.
- `minZoom`: The minimum zoom level allowed when zooming out. Default: `0.1{:ts}`.
- `maxZoom`: The maximum zoom level allowed when zooming in. Default: `5{:ts}`.
- `defaultFullscreen`: Whether canvas pages default to fullscreen mode. When enabled, the canvas fills the entire viewport on load. Users can toggle fullscreen with the button in the top-right corner, or press `Escape` to exit. Default: `false{:ts}`.
### Features
- **Text nodes**: Render Markdown content including headings, bold, italic, strikethrough, lists, links, and code blocks via [GFM](https://github.github.com/gfm/) support.
- **File nodes**: Link to other pages in your vault. Supports popover previews on hover.
- **Link nodes**: Reference external URLs.
- **Group nodes**: Visual grouping containers with optional labels and background colors.
- **Edges**: SVG connections between nodes with optional labels, arrow markers, and colors. Supports all four sides (top, right, bottom, left) and both preset colors (16) and custom hex colors.
- **Fullscreen mode**: Toggle button to expand the canvas to fill the viewport. Configurable default via `defaultFullscreen`.
- **Preset colors**: Six preset colors (red, orange, yellow, green, cyan, purple) plus custom hex colors (`#RRGGBB`) for nodes and edges.
## API
- Category: Page Type
- Function name: `ExternalPlugin.CanvasPage()`.
- Source: [`quartz-community/canvas-page`](https://github.com/quartz-community/canvas-page)
- Install: `npx quartz plugin add github:quartz-community/canvas-page`

2
package-lock.json generated
View File

@ -1934,7 +1934,7 @@
},
"node_modules/@quartz-community/types": {
"version": "0.2.1",
"resolved": "git+ssh://git@github.com/quartz-community/types.git#307f7393d96e8514c042307e7fbfb47ae7a2b330",
"resolved": "git+ssh://git@github.com/quartz-community/types.git#c6d2b15bdf2a6097a5d748c74caf406c41ac755b",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -89,6 +89,7 @@ const config: QuartzConfig = {
ExternalPlugin.CNAME(),
],
pageTypes: [
ExternalPlugin.CanvasPage(),
ExternalPlugin.ContentPage(),
ExternalPlugin.FolderPage(),
ExternalPlugin.TagPage(),
@ -130,6 +131,7 @@ const config: QuartzConfig = {
"github:quartz-community/favicon",
"github:quartz-community/content-index",
"github:quartz-community/og-image",
"github:quartz-community/canvas-page",
],
}

View File

@ -114,5 +114,24 @@ export const layout: {
left: [],
right: [],
},
// Canvas pages — expansive layout, no sidebars
canvas: {
beforeBody: [breadcrumbsComponent, articleTitleComponent],
left: [
pageTitleComponent,
Component.MobileOnly(Component.Spacer()),
Component.Flex({
components: [
{
Component: searchComponent,
grow: true,
},
{ Component: darkmodeComponent },
],
}),
],
right: [],
},
},
}

View File

@ -216,6 +216,12 @@
"resolved": "https://github.com/quartz-community/og-image.git",
"commit": "c72ed66e951663a40bd2d0834725090572ffb124",
"installedAt": "2026-02-14T01:16:33.273Z"
},
"canvas-page": {
"source": "github:quartz-community/canvas-page",
"resolved": "https://github.com/quartz-community/canvas-page.git",
"commit": "08b05cc9a5ebc2897793b81fc0c3de3b39b7259d",
"installedAt": "2026-02-14T14:31:17.985Z"
}
}
}

View File

@ -1,14 +1,30 @@
import { FilePath, joinSegments, slugifyFilePath } from "../../util/path"
import { QuartzEmitterPlugin } from "../types"
import { QuartzEmitterPlugin, QuartzPageTypePluginInstance } from "../types"
import path from "path"
import fs from "fs"
import { glob } from "../../util/glob"
import { Argv } from "../../util/ctx"
import { Argv, BuildCtx } from "../../util/ctx"
import { QuartzConfig } from "../../cfg"
const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => {
// glob all non MD files in content folder and copy it over
return await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
function getPageTypeExtensions(ctx: BuildCtx): Set<string> {
const extensions = new Set<string>()
const pageTypes = (ctx.cfg.plugins.pageTypes ?? []) as unknown as QuartzPageTypePluginInstance[]
for (const pt of pageTypes) {
if (pt.fileExtensions) {
for (const ext of pt.fileExtensions) {
extensions.add(ext)
}
}
}
return extensions
}
const filesToCopy = async (argv: Argv, cfg: QuartzConfig, excludeExtensions: Set<string>) => {
const excludePatterns = ["**/*.md", ...cfg.configuration.ignorePatterns]
for (const ext of excludeExtensions) {
excludePatterns.push(`**/*${ext}`)
}
return await glob("**", argv.directory, excludePatterns)
}
const copyFile = async (argv: Argv, fp: FilePath) => {
@ -17,7 +33,6 @@ const copyFile = async (argv: Argv, fp: FilePath) => {
const name = slugifyFilePath(fp)
const dest = joinSegments(argv.output, name) as FilePath
// ensure dir exists
const dir = path.dirname(dest) as FilePath
await fs.promises.mkdir(dir, { recursive: true })
@ -28,16 +43,18 @@ const copyFile = async (argv: Argv, fp: FilePath) => {
export const Assets: QuartzEmitterPlugin = () => {
return {
name: "Assets",
async *emit({ argv, cfg }) {
const fps = await filesToCopy(argv, cfg)
async *emit(ctx) {
const excludeExtensions = getPageTypeExtensions(ctx)
const fps = await filesToCopy(ctx.argv, ctx.cfg, excludeExtensions)
for (const fp of fps) {
yield copyFile(argv, fp)
yield copyFile(ctx.argv, fp)
}
},
async *partialEmit(ctx, _content, _resources, changeEvents) {
const excludeExtensions = getPageTypeExtensions(ctx)
for (const changeEvent of changeEvents) {
const ext = path.extname(changeEvent.path)
if (ext === ".md") continue
if (ext === ".md" || excludeExtensions.has(ext)) continue
if (changeEvent.type === "add" || changeEvent.type === "change") {
yield copyFile(ctx.argv, changeEvent.path)

View File

@ -97,6 +97,7 @@ export type QuartzPageTypePlugin<Options extends OptionType = undefined> = (
export interface QuartzPageTypePluginInstance {
name: string
priority?: number
fileExtensions?: string[]
match: PageMatcher
generate?: PageGenerator
layout: string
@ -112,6 +113,7 @@ export interface QuartzPageTypePluginInstance {
export interface PageTypePluginEntry {
name: string
priority?: number
fileExtensions?: string[]
match: (...args: never[]) => boolean
generate?: (...args: never[]) => VirtualPage[]
layout: string