diff --git a/content/example-canvas.canvas b/content/example-canvas.canvas new file mode 100644 index 000000000..b688adcf1 --- /dev/null +++ b/content/example-canvas.canvas @@ -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" + } + ] +} diff --git a/content/index.md b/content/index.md new file mode 100644 index 000000000..f030bec69 --- /dev/null +++ b/content/index.md @@ -0,0 +1,7 @@ +--- +title: Welcome +--- + +This is a test site for Quartz v5. + +- [[example-canvas|Example Canvas]] diff --git a/docs/plugins/CanvasPage.canvas b/docs/plugins/CanvasPage.canvas new file mode 100644 index 000000000..390cb6172 --- /dev/null +++ b/docs/plugins/CanvasPage.canvas @@ -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** (1–6) 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" + } + ] +} diff --git a/docs/plugins/CanvasPage.md b/docs/plugins/CanvasPage.md new file mode 100644 index 000000000..0d6139192 --- /dev/null +++ b/docs/plugins/CanvasPage.md @@ -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 (1–6) 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` diff --git a/package-lock.json b/package-lock.json index 83e943f1c..9140a54a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/quartz.config.ts b/quartz.config.ts index a8410ddd5..ee3d4393f 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -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", ], } diff --git a/quartz.layout.ts b/quartz.layout.ts index b55569e16..c4148d37f 100644 --- a/quartz.layout.ts +++ b/quartz.layout.ts @@ -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: [], + }, }, } diff --git a/quartz.lock.json b/quartz.lock.json index 489543b8f..668d67f01 100644 --- a/quartz.lock.json +++ b/quartz.lock.json @@ -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" } } -} +} \ No newline at end of file diff --git a/quartz/plugins/emitters/assets.ts b/quartz/plugins/emitters/assets.ts index d0da66ace..e57707e35 100644 --- a/quartz/plugins/emitters/assets.ts +++ b/quartz/plugins/emitters/assets.ts @@ -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 { + const extensions = new Set() + 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) => { + 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) diff --git a/quartz/plugins/types.ts b/quartz/plugins/types.ts index 24bb5443e..abe576164 100644 --- a/quartz/plugins/types.ts +++ b/quartz/plugins/types.ts @@ -97,6 +97,7 @@ export type QuartzPageTypePlugin = ( 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