From 73aa1376ce30692457926bcda41aaa8c0175fd1b Mon Sep 17 00:00:00 2001 From: kzoeps Date: Thu, 11 Jun 2026 15:08:00 +0600 Subject: [PATCH] docs: add remote markdown and mermaid rendering --- components/CopyRawButton.js | 10 +- components/Layout.js | 2 +- components/MermaidDiagram.js | 116 +++ components/RemoteMarkdown.js | 274 +++++++ components/TableOfContents.js | 55 +- docs/remote-markdown.md | 33 + lib/generate-raw-pages.js | 45 +- lib/generate-search-index.js | 89 ++- markdoc/nodes/fence.markdoc.js | 20 + markdoc/tags/index.js | 2 + markdoc/tags/remote-doc.markdoc.js | 12 + package-lock.json | 1094 ++++++++++++++++++++++++++++ package.json | 1 + pages/_app.js | 5 + pages/architecture/epds.md | 332 +-------- styles/globals.css | 56 ++ 16 files changed, 1755 insertions(+), 391 deletions(-) create mode 100644 components/MermaidDiagram.js create mode 100644 components/RemoteMarkdown.js create mode 100644 docs/remote-markdown.md create mode 100644 markdoc/tags/remote-doc.markdoc.js diff --git a/components/CopyRawButton.js b/components/CopyRawButton.js index b8ec7b3..76f3b13 100644 --- a/components/CopyRawButton.js +++ b/components/CopyRawButton.js @@ -1,18 +1,22 @@ import { useState } from 'react'; import { useRouter } from 'next/router'; -function getRawUrl(currentPath) { +function getGeneratedRawUrl(currentPath) { if (currentPath === '/') return '/raw/index.md'; return `/raw${currentPath}.md`; } -export function CopyRawButton() { +/** + * Render page-level actions for copying or viewing the Markdown source for the current docs page. + * Pages that render canonical Markdown from another repository can set `rawUrl` in frontmatter so these actions use that source instead of the generated local fallback. + */ +export function CopyRawButton({ frontmatter }) { const [copied, setCopied] = useState(false); const [copyError, setCopyError] = useState(false); const [isCopying, setIsCopying] = useState(false); const router = useRouter(); const currentPath = router.asPath.split('#')[0].split('?')[0] || '/'; - const rawUrl = getRawUrl(currentPath); + const rawUrl = frontmatter?.rawUrl || getGeneratedRawUrl(currentPath); const handleCopy = async () => { setIsCopying(true); diff --git a/components/Layout.js b/components/Layout.js index a5e0dc4..39601ec 100644 --- a/components/Layout.js +++ b/components/Layout.js @@ -201,7 +201,7 @@ export default function Layout({ children, frontmatter }) {
- {frontmatter && } + {frontmatter && }
{children}
diff --git a/components/MermaidDiagram.js b/components/MermaidDiagram.js new file mode 100644 index 0000000..0809191 --- /dev/null +++ b/components/MermaidDiagram.js @@ -0,0 +1,116 @@ +import { useEffect, useMemo, useState } from 'react'; +import { CodeBlock } from './CodeBlock'; + +let mermaidModulePromise; +let diagramIdCounter = 0; + +function getMermaid() { + if (!mermaidModulePromise) { + mermaidModulePromise = import('mermaid').then((module) => { + const mermaid = module.default || module; + return mermaid; + }); + } + + return mermaidModulePromise; +} + +function getDiagramId() { + diagramIdCounter += 1; + return `mermaid-diagram-${diagramIdCounter}`; +} + +function getPreferredMermaidTheme() { + if (typeof document !== 'undefined' && document.documentElement.classList.contains('dark')) { + return 'dark'; + } + + return 'neutral'; +} + +/** + * Render a Mermaid fenced code block as an SVG diagram in the browser. + * Use this for Markdown fences with `mermaid` as the language; invalid diagrams fall back to copyable code with an actionable syntax error. + */ +export function MermaidDiagram({ chart, children }) { + const source = (chart || children || '').trim(); + const diagramId = useMemo(getDiagramId, []); + const [svg, setSvg] = useState(''); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(Boolean(source)); + const [theme, setTheme] = useState('neutral'); + + useEffect(() => { + const updateTheme = () => setTheme(getPreferredMermaidTheme()); + updateTheme(); + + const observer = new MutationObserver(updateTheme); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); + + return () => observer.disconnect(); + }, []); + + useEffect(() => { + if (!source) { + setLoading(false); + setError(new Error('No Mermaid source was provided. Add diagram text inside the mermaid code fence.')); + return undefined; + } + + let cancelled = false; + setLoading(true); + setError(null); + setSvg(''); + + async function renderDiagram() { + try { + const mermaid = await getMermaid(); + mermaid.initialize({ + startOnLoad: false, + securityLevel: 'strict', + theme, + }); + const result = await mermaid.render(diagramId, source); + if (cancelled) return; + setSvg(result.svg); + } catch (err) { + if (cancelled) return; + setError(err); + } finally { + if (!cancelled) setLoading(false); + } + } + + renderDiagram(); + + return () => { + cancelled = true; + }; + }, [diagramId, source, theme]); + + if (error) { + return ( +
+

+ Could not render Mermaid diagram. Check the diagram syntax in the source Markdown and reload the page. Details: {error.message} +

+ +
+ ); + } + + if (loading) { + return ( +
+ Rendering Mermaid diagram… +
+ ); + } + + return ( +
+ ); +} diff --git a/components/RemoteMarkdown.js b/components/RemoteMarkdown.js new file mode 100644 index 0000000..94b6592 --- /dev/null +++ b/components/RemoteMarkdown.js @@ -0,0 +1,274 @@ +import React, { useEffect, useMemo, useState, useContext } from 'react'; +import { useRouter } from 'next/router'; +import Markdoc from '@markdoc/markdoc'; +import tags from '../markdoc/tags'; +import { fence, heading, link } from '../markdoc/nodes'; +import { Callout } from './Callout'; +import { Columns } from './Columns'; +import { Column } from './Column'; +import { Figure } from './Figure'; +import { Heading } from './Heading'; +import { CardLink } from './CardLink'; +import { CodeBlock } from './CodeBlock'; +import { Link } from './Link'; +import { DotPattern } from './DotPattern'; +import { HeroBanner } from './HeroBanner'; +import { CardGrid } from './CardGrid'; +import { MermaidDiagram } from './MermaidDiagram'; + +const SOURCE_ORG = 'hypercerts-org'; +const RemoteMarkdownContext = React.createContext(null); + +const markdocConfig = { + tags, + nodes: { fence, heading, link }, +}; + +/** + * Convert an allowed GitHub Markdown URL into the raw URL used by the browser fetch. + * Only hypercerts-org GitHub sources are allowed so docs pages cannot become an open proxy. + */ +function resolveGitHubMarkdownSource(source) { + const url = new URL(source); + + if (url.hostname === 'github.com') { + const [, owner, repo, marker, ref, ...fileParts] = url.pathname.split('/'); + if (owner?.toLowerCase() !== SOURCE_ORG || marker !== 'blob' || !repo || !ref || fileParts.length === 0) { + throw new Error('Remote docs must use a hypercerts-org GitHub blob URL, for example https://github.com/hypercerts-org/ePDS/blob/main/docs/tutorial.md.'); + } + + const filePath = fileParts.join('/'); + return { + sourceUrl: url.toString(), + rawUrl: `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${filePath}`, + owner, + repo, + ref, + filePath, + }; + } + + if (url.hostname === 'raw.githubusercontent.com') { + const [, owner, repo, ref, ...fileParts] = url.pathname.split('/'); + if (owner?.toLowerCase() !== SOURCE_ORG || !repo || !ref || fileParts.length === 0) { + throw new Error('Remote docs must use a raw.githubusercontent.com URL under hypercerts-org.'); + } + + const filePath = fileParts.join('/'); + return { + sourceUrl: `https://github.com/${owner}/${repo}/blob/${ref}/${filePath}`, + rawUrl: url.toString(), + owner, + repo, + ref, + filePath, + }; + } + + throw new Error('Remote docs can only be loaded from github.com or raw.githubusercontent.com.'); +} + +/** + * Remove optional YAML frontmatter before parsing remote Markdown with Markdoc. + */ +function stripFrontmatter(markdown) { + if (!markdown.startsWith('---\n')) return markdown; + + const end = markdown.indexOf('\n---\n', 4); + if (end === -1) return markdown; + + return markdown.slice(end + 5).trimStart(); +} + +/** + * Resolve relative links in remotely-rendered docs back to the source GitHub repo. + */ +function resolveRemoteHref(href, source) { + if (!href || href.startsWith('#') || href.startsWith('/') || /^[a-z][a-z0-9+.-]*:/i.test(href)) { + return href; + } + + const sourceDir = source.filePath.split('/').slice(0, -1).join('/'); + const basePath = `/${source.owner}/${source.repo}/blob/${source.ref}/${sourceDir ? `${sourceDir}/` : ''}`; + const resolved = new URL(href, `https://github.com${basePath}`); + const [, owner, repo, , ref, ...repoPathParts] = resolved.pathname.split('/'); + const repoPath = repoPathParts.join('/'); + const lastSegment = repoPathParts[repoPathParts.length - 1] || ''; + const mode = /\.[a-z0-9]+$/i.test(lastSegment) ? 'blob' : 'tree'; + + return `https://github.com/${owner}/${repo}/${mode}/${ref}/${repoPath}${resolved.hash}`; +} + +function RemoteLink({ href, children, ...props }) { + const source = useContext(RemoteMarkdownContext); + const resolvedHref = source ? resolveRemoteHref(href, source) : href; + + return ( + + {children} + + ); +} + +const remoteMarkdocComponents = { + Callout, + Columns, + Column, + Figure, + Heading, + CardLink, + CodeBlock, + Fence: CodeBlock, + Link: RemoteLink, + DotPattern, + HeroBanner, + CardGrid, + MermaidDiagram, +}; + +function getRawCacheUrl(currentPath) { + if (currentPath === '/') return '/raw/index.md'; + return `/raw${currentPath.replace(/\.html$/, '')}.md`; +} + +async function fetchMarkdown(url, signal, errorPrefix) { + const response = await fetch(url, { + cache: 'no-store', + signal, + }); + + if (!response.ok) { + throw new Error(`${errorPrefix} returned ${response.status} ${response.statusText || 'without the Markdown file'}.`); + } + + return response.text(); +} + +function transformMarkdown(markdown) { + const ast = Markdoc.parse(stripFrontmatter(markdown)); + return Markdoc.transform(ast, markdocConfig); +} + +/** + * Runtime Markdoc renderer for Markdown files that live in Hypercerts service repos. + * It tries the live GitHub raw source first, then the build-time `/raw` cache, then the wrapped local fallback. + */ +export function RemoteMarkdown({ source, children }) { + const router = useRouter(); + const currentPath = router.asPath.split('#')[0].split('?')[0] || '/'; + const rawCacheUrl = getRawCacheUrl(currentPath); + const [markdown, setMarkdown] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const sourceState = useMemo(() => { + try { + return { sourceInfo: resolveGitHubMarkdownSource(source), sourceError: null }; + } catch (err) { + return { sourceInfo: null, sourceError: err }; + } + }, [source]); + const { sourceInfo, sourceError } = sourceState; + + useEffect(() => { + if (!sourceInfo) { + setIsLoading(false); + return undefined; + } + + const controller = new AbortController(); + setMarkdown(null); + setError(null); + setIsLoading(true); + + async function loadRemoteMarkdown() { + let githubError = null; + + try { + const nextMarkdown = await fetchMarkdown(sourceInfo.rawUrl, controller.signal, 'GitHub'); + transformMarkdown(nextMarkdown); + setMarkdown(nextMarkdown); + return; + } catch (err) { + if (err.name === 'AbortError') return; + githubError = err; + } + + try { + const cachedMarkdown = await fetchMarkdown(rawCacheUrl, controller.signal, 'Build-time raw cache'); + transformMarkdown(cachedMarkdown); + setMarkdown(cachedMarkdown); + } catch (err) { + if (err.name === 'AbortError') return; + setError(new Error(`${githubError.message} The build-time raw cache also failed: ${err.message}`)); + } finally { + setIsLoading(false); + } + } + + loadRemoteMarkdown().finally(() => { + if (!controller.signal.aborted) setIsLoading(false); + }); + + return () => controller.abort(); + }, [rawCacheUrl, sourceInfo]); + + const renderedState = useMemo(() => { + if (!markdown || !sourceInfo) return { renderedContent: null, renderError: null }; + + try { + const content = transformMarkdown(markdown); + return { + renderedContent: Markdoc.renderers.react(content, React, { + components: remoteMarkdocComponents, + }), + renderError: null, + }; + } catch (err) { + return { renderedContent: null, renderError: err }; + } + }, [markdown, sourceInfo]); + const { renderedContent, renderError } = renderedState; + const displayError = error || sourceError || renderError; + + useEffect(() => { + if (!renderedContent) return undefined; + + const frame = window.requestAnimationFrame(() => { + window.dispatchEvent(new CustomEvent('remote-docs:loaded')); + }); + + return () => window.cancelAnimationFrame(frame); + }, [renderedContent]); + + if (renderedContent) { + return ( + + {renderedContent} + + ); + } + + if (isLoading && !displayError) { + return ( +
+ Loading the canonical docs from GitHub… +
+ ); + } + + return ( + <> +
+ Could not load the canonical docs.{' '} + The browser could not load the live GitHub raw file or the build-time raw cache. Showing the local fallback below. Try refreshing, or edit{' '} + {sourceInfo ? ( + the source file + ) : ( + 'the configured source URL' + )}{' '} + if the URL is wrong. Details: {displayError?.message || 'No renderable Markdown source was available.'} +
+ {children} + + ); +} diff --git a/components/TableOfContents.js b/components/TableOfContents.js index 21f78ba..71ca2ac 100644 --- a/components/TableOfContents.js +++ b/components/TableOfContents.js @@ -8,31 +8,44 @@ export function TableOfContents() { const router = useRouter(); const currentPath = router.asPath.split("#")[0].split("?")[0]; - // Extract H2 and H3 headings from the DOM after render + // Extract H2 and H3 headings from the DOM after render and after runtime docs load. useEffect(() => { - if (typeof window === "undefined") return; + if (typeof window === "undefined") return undefined; const article = document.querySelector(".layout-content article"); if (!article) { setHeadings([]); - return; + return undefined; } - const elements = article.querySelectorAll("h2, h3"); - const items = Array.from(elements).map((el) => { - if (!el.id) { - el.id = el.textContent - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/(^-|-$)/g, ""); - } - return { - id: el.id, - text: el.textContent, - level: el.tagName === "H3" ? 3 : 2, - }; - }); - setHeadings(items); - setActiveId(""); + const collectHeadings = () => { + const elements = article.querySelectorAll("h2, h3, h4"); + const items = Array.from(elements).map((el) => { + if (!el.id) { + el.id = el.textContent + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, ""); + } + return { + id: el.id, + text: el.textContent, + level: Number(el.tagName.slice(1)), + }; + }); + setHeadings(items); + setActiveId(""); + }; + + collectHeadings(); + + const observer = new MutationObserver(collectHeadings); + observer.observe(article, { childList: true, subtree: true }); + window.addEventListener("remote-docs:loaded", collectHeadings); + + return () => { + observer.disconnect(); + window.removeEventListener("remote-docs:loaded", collectHeadings); + }; }, [currentPath]); // Scroll spy using scroll position @@ -94,8 +107,8 @@ export function TableOfContents() { { e.preventDefault(); const target = document.getElementById(id); diff --git a/docs/remote-markdown.md b/docs/remote-markdown.md new file mode 100644 index 0000000..1139217 --- /dev/null +++ b/docs/remote-markdown.md @@ -0,0 +1,33 @@ +# Runtime remote Markdown + +Use the `{% remote-doc %}` Markdoc tag when a page should render canonical Markdown from a Hypercerts service repository without copying that Markdown into this documentation repo. + +```md +--- +rawUrl: "https://raw.githubusercontent.com/hypercerts-org/ePDS/main/docs/tutorial.md" +--- + +{% remote-doc source="https://github.com/hypercerts-org/ePDS/blob/main/docs/tutorial.md" %} + +A short unavailable-state fallback goes here. Keep it brief so the documentation repo does not become a second source of truth. + +{% /remote-doc %} +``` + +## How it works + +- The site remains a static Next.js export. +- The browser fetches the source file from `raw.githubusercontent.com` at runtime first. +- If the live GitHub fetch fails, the browser falls back to the build-time copy in `/raw`. +- The fetched Markdown is parsed with the same Markdoc tags and nodes used by local pages. +- Fenced `mermaid` diagrams render as SVGs in the browser through the Mermaid npm package. +- Relative links in the remote Markdown point back to the source GitHub repository. +- `Copy raw` and `View raw` use the page's `rawUrl` frontmatter when it is set. +- The wrapped local Markdown is the last-resort fallback content. Keep it to a short unavailable-state message, not a copy of the canonical docs. + +## Constraints + +- Sources must be in `hypercerts-org` GitHub repositories. +- The current implementation is browser-runtime fetching, not server-side rendering. +- Build-time search and raw-page generation fetch `rawUrl`, so search and `/raw` use the canonical Markdown instead of the local fallback. +- If runtime docs become permanent for many pages, add build-time indexing for the remote sources too. diff --git a/lib/generate-raw-pages.js b/lib/generate-raw-pages.js index bda1cbc..a3ed888 100644 --- a/lib/generate-raw-pages.js +++ b/lib/generate-raw-pages.js @@ -33,15 +33,44 @@ function getRawOutputPath(filePath) { return join(OUTPUT_DIR, outputRel); } -rmSync(OUTPUT_DIR, { recursive: true, force: true }); -mkdirSync(OUTPUT_DIR, { recursive: true }); +function getFrontmatterRawUrl(markdown) { + const match = markdown.match(/^---\n([\s\S]*?)\n---\n/); + if (!match) return null; -const files = walkDir(PAGES_DIR); + const rawUrlMatch = match[1].match(/^rawUrl:\s*["']?([^"'\n]+)["']?\s*$/m); + return rawUrlMatch?.[1] || null; +} + +async function getRawMarkdown(file) { + const localMarkdown = readFileSync(file, 'utf-8'); + const rawUrl = getFrontmatterRawUrl(localMarkdown); + + if (!rawUrl) return localMarkdown; + + const response = await fetch(rawUrl, { cache: 'no-store' }); + if (!response.ok) { + throw new Error(`Failed to fetch rawUrl for ${relative(PAGES_DIR, file)}: ${rawUrl} returned ${response.status} ${response.statusText || ''}`.trim()); + } + + return response.text(); +} + +async function main() { + rmSync(OUTPUT_DIR, { recursive: true, force: true }); + mkdirSync(OUTPUT_DIR, { recursive: true }); + + const files = walkDir(PAGES_DIR); + + for (const file of files) { + const outputPath = getRawOutputPath(file); + mkdirSync(dirname(outputPath), { recursive: true }); + writeFileSync(outputPath, await getRawMarkdown(file)); + } -for (const file of files) { - const outputPath = getRawOutputPath(file); - mkdirSync(dirname(outputPath), { recursive: true }); - writeFileSync(outputPath, readFileSync(file, 'utf-8')); + console.log(`Generated raw markdown files for ${files.length} pages`); } -console.log(`Generated raw markdown files for ${files.length} pages`); +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/lib/generate-search-index.js b/lib/generate-search-index.js index e6afca6..45377b2 100644 --- a/lib/generate-search-index.js +++ b/lib/generate-search-index.js @@ -18,17 +18,23 @@ function walkDir(dir) { return results; } +function stripYamlValue(value) { + return value.trim().replace(/^['"]|['"]$/g, ""); +} + function extractFrontmatter(content) { const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); - if (!fmMatch) return { title: "", description: "" }; + if (!fmMatch) return { title: "", description: "", rawUrl: "" }; const frontmatter = fmMatch[1]; const titleMatch = frontmatter.match(/^title:\s*(.+)$/m); const descMatch = frontmatter.match(/^description:\s*(.+)$/m); + const rawUrlMatch = frontmatter.match(/^rawUrl:\s*(.+)$/m); return { - title: titleMatch ? titleMatch[1].trim() : "", - description: descMatch ? descMatch[1].trim() : "", + title: titleMatch ? stripYamlValue(titleMatch[1]) : "", + description: descMatch ? stripYamlValue(descMatch[1]) : "", + rawUrl: rawUrlMatch ? stripYamlValue(rawUrlMatch[1]) : "", }; } @@ -100,40 +106,59 @@ function getSection(path) { return "Other"; } -const files = walkDir(PAGES_DIR); -const index = []; +async function getIndexContent(file, localContent, rawUrl) { + if (!rawUrl) return localContent; -for (const file of files) { - const content = readFileSync(file, "utf-8"); - const rel = "/" + relative(PAGES_DIR, file).replace(/\.md$/, ""); - const path = rel === "/index" ? "/" : rel; + const response = await fetch(rawUrl, { cache: "no-store" }); + if (!response.ok) { + throw new Error(`Failed to fetch rawUrl for search index ${relative(PAGES_DIR, file)}: ${rawUrl} returned ${response.status} ${response.statusText || ""}`.trim()); + } - const { title, description } = extractFrontmatter(content); - const headings = extractHeadings(content); - const section = getSection(path); + return response.text(); +} - // For the home page, only include title (body is mostly card markup) - let body = ""; - if (path !== "/") { - body = stripMarkdown(content); - if (body.length > MAX_BODY_LENGTH) { - body = body.substring(0, MAX_BODY_LENGTH); +async function main() { + const files = walkDir(PAGES_DIR); + const index = []; + + for (const file of files) { + const localContent = readFileSync(file, "utf-8"); + const rel = "/" + relative(PAGES_DIR, file).replace(/\.md$/, ""); + const path = rel === "/index" ? "/" : rel; + + const { title, description, rawUrl } = extractFrontmatter(localContent); + const content = await getIndexContent(file, localContent, rawUrl); + const headings = extractHeadings(content); + const section = getSection(path); + + // For the home page, only include title (body is mostly card markup) + let body = ""; + if (path !== "/") { + body = stripMarkdown(content); + if (body.length > MAX_BODY_LENGTH) { + body = body.substring(0, MAX_BODY_LENGTH); + } } + + index.push({ + path, + title, + description: description || "", + section, + headings, + body, + }); } - index.push({ - path, - title, - description: description || "", - section, - headings, - body, - }); + writeFileSync(OUTPUT, JSON.stringify(index, null, 2) + "\n"); + console.log( + `Generated search index for ${index.length} pages (${ + Buffer.byteLength(JSON.stringify(index)) / 1024 + } KB)` + ); } -writeFileSync(OUTPUT, JSON.stringify(index, null, 2) + "\n"); -console.log( - `Generated search index for ${index.length} pages (${ - Buffer.byteLength(JSON.stringify(index)) / 1024 - } KB)` -); +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/markdoc/nodes/fence.markdoc.js b/markdoc/nodes/fence.markdoc.js index f154ba3..8595669 100644 --- a/markdoc/nodes/fence.markdoc.js +++ b/markdoc/nodes/fence.markdoc.js @@ -1,3 +1,12 @@ +import { Tag } from "@markdoc/markdoc"; + +function getFenceLanguage(language) { + return String(language || "") + .trim() + .toLowerCase() + .split(/\s+/)[0]; +} + const fence = { render: "CodeBlock", attributes: { @@ -14,6 +23,17 @@ const fence = { default: true, }, }, + transform(node, config) { + const attributes = node.transformAttributes(config); + + if (getFenceLanguage(attributes.language) === "mermaid") { + return new Tag("MermaidDiagram", { + chart: attributes.content, + }); + } + + return new Tag("CodeBlock", attributes); + }, }; export default fence; diff --git a/markdoc/tags/index.js b/markdoc/tags/index.js index c494cae..00e6429 100644 --- a/markdoc/tags/index.js +++ b/markdoc/tags/index.js @@ -5,6 +5,7 @@ import figure from './figure.markdoc'; import cardLink from './card-link.markdoc'; import cardGrid from './card-grid.markdoc'; import heroBanner from './hero-banner.markdoc'; +import remoteDoc from './remote-doc.markdoc'; export default { callout, @@ -14,4 +15,5 @@ export default { 'card-link': cardLink, 'card-grid': cardGrid, 'hero-banner': heroBanner, + 'remote-doc': remoteDoc, }; diff --git a/markdoc/tags/remote-doc.markdoc.js b/markdoc/tags/remote-doc.markdoc.js new file mode 100644 index 0000000..630d2d7 --- /dev/null +++ b/markdoc/tags/remote-doc.markdoc.js @@ -0,0 +1,12 @@ +/** + * Markdoc tag for rendering canonical Markdown from a Hypercerts GitHub repo at browser runtime. + */ +module.exports = { + render: 'RemoteMarkdown', + attributes: { + source: { + type: String, + required: true, + }, + }, +}; diff --git a/package-lock.json b/package-lock.json index ceb1af4..6bc12aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,38 @@ "@markdoc/next.js": "^0.5.0", "@vercel/analytics": "^1.6.1", "flexsearch": "^0.8.212", + "mermaid": "^11.15.0", "next": "^16.1.6", "prism-react-renderer": "^2.4.1", "react": "^19.2.4", "react-dom": "^19.2.4" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, "node_modules/@emnapi/runtime": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", @@ -28,6 +54,23 @@ "tslib": "^2.4.0" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.3.tgz", + "integrity": "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@img/colour": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", @@ -533,6 +576,15 @@ "react": "*" } }, + "node_modules/@mermaid-js/parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.1.tgz", + "integrity": "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.1" + } + }, "node_modules/@next/env": { "version": "16.1.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", @@ -676,6 +728,265 @@ "tslib": "^2.8.0" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/linkify-it": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", @@ -707,6 +1018,23 @@ "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@vercel/analytics": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.6.1.tgz", @@ -795,6 +1123,538 @@ "node": ">=6" } }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cytoscape": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", + "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -805,6 +1665,25 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.9.tgz", + "integrity": "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/es-toolkit": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz", + "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/flexsearch": { "version": "0.8.212", "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.8.212.tgz", @@ -833,6 +1712,43 @@ ], "license": "Apache-2.0" }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -845,6 +1761,89 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mermaid": { + "version": "11.15.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.15.0.tgz", + "integrity": "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.1.1", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.1", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.19", + "dompurify": "^3.3.1", + "es-toolkit": "^1.45.1", + "katex": "^0.16.25", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -916,12 +1915,40 @@ } } }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -984,6 +2011,36 @@ "react": "^19.2.4" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -1080,11 +2137,48 @@ } } }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" + }, + "node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } } } } diff --git a/package.json b/package.json index da60b12..8c8d29e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@markdoc/next.js": "^0.5.0", "@vercel/analytics": "^1.6.1", "flexsearch": "^0.8.212", + "mermaid": "^11.15.0", "next": "^16.1.6", "prism-react-renderer": "^2.4.1", "react": "^19.2.4", diff --git a/pages/_app.js b/pages/_app.js index 3422f00..6c6168f 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -11,6 +11,8 @@ import { Link } from '../components/Link'; import { DotPattern } from '../components/DotPattern'; import { HeroBanner } from '../components/HeroBanner'; import { CardGrid } from '../components/CardGrid'; +import { RemoteMarkdown } from '../components/RemoteMarkdown'; +import { MermaidDiagram } from '../components/MermaidDiagram'; import { Analytics } from '@vercel/analytics/next'; const components = { @@ -26,6 +28,9 @@ const components = { DotPattern, HeroBanner, CardGrid, + RemoteMarkdown, + RemoteDoc: RemoteMarkdown, + MermaidDiagram, }; export default function App({ Component, pageProps }) { diff --git a/pages/architecture/epds.md b/pages/architecture/epds.md index a2cfb61..f2d5159 100644 --- a/pages/architecture/epds.md +++ b/pages/architecture/epds.md @@ -1,335 +1,15 @@ --- title: ePDS (extended PDS) description: How the ePDS adds email/OTP login on top of AT Protocol without changing the standard OAuth flow for apps. +rawUrl: "https://raw.githubusercontent.com/hypercerts-org/ePDS/main/docs/tutorial.md" --- -# ePDS (extended PDS) +{% remote-doc source="https://github.com/hypercerts-org/ePDS/blob/main/docs/tutorial.md" %} -The ePDS adds email-based, passwordless sign-in on top of a standard AT Protocol PDS. Users enter their email, receive a one-time code, and end up with a normal AT Protocol session tied to a DID. +# ePDS docs unavailable -Certified operates production, staging, and test ePDS instances. See [Certified services](/reference/certified-pdss) for the current hostnames and guidance on which to use in which scenario. +The canonical ePDS docs could not be loaded from GitHub or the build-time raw cache. -For applications, the important part is that ePDS still finishes by issuing a standard AT Protocol authorization code. In practice, this means you can integrate it with [`@atproto/oauth-client-node`](https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-node). +View the source directly: https://github.com/hypercerts-org/ePDS/blob/main/docs/tutorial.md -## System overview - -```text -Client App - -> starts AT Protocol OAuth against the PDS - -PDS Core - -> remains the OAuth issuer and token endpoint - -> advertises the Auth Service as the authorization endpoint - -Auth Service - -> collects the user's email or OTP - -> verifies the user - -> returns control to PDS Core via signed callback - -PDS Core - -> issues a normal authorization code - -Client App - -> exchanges the code for tokens -``` - -The PDS remains the OAuth issuer and token endpoint. The main difference is that the authorization step happens on the ePDS Auth Service, which handles the email and OTP flow before returning control to the PDS. - -## Integrating with `@atproto/oauth-client-node` - -ePDS works with the standard AT Protocol OAuth client libraries. The main ePDS-specific behavior is how you shape the authorization URL before redirecting the user. - -### Flow 1: your app collects the email - -In Flow 1, your app has its own email field. Start OAuth normally, then add `login_hint=` to the authorization URL before redirecting the user. - -```ts -import { NodeOAuthClient } from '@atproto/oauth-client-node' - -const oauthClient = new NodeOAuthClient({ - clientMetadata: { - client_id: 'https://yourapp.example.com/client-metadata.json', - client_name: 'Your App', - client_uri: 'https://yourapp.example.com', - redirect_uris: ['https://yourapp.example.com/api/oauth/callback'], - scope: 'atproto transition:generic', - grant_types: ['authorization_code', 'refresh_token'], - response_types: ['code'], - token_endpoint_auth_method: 'none', - dpop_bound_access_tokens: true, - }, - stateStore, - sessionStore, -}) - -const url = await oauthClient.authorize('alice.certified.one', { - scope: 'atproto transition:generic', -}) - -// ePDS-specific customization happens here. -const authUrl = new URL(url) -authUrl.searchParams.set('login_hint', email) -authUrl.searchParams.set('epds_handle_mode', 'picker-with-random') - -return authUrl.toString() -``` - -{% callout type="warning" %} -Do not put an email address into the PAR body as `login_hint`. For ePDS, add `login_hint` to the authorization URL instead. -{% /callout %} - -With `login_hint` set, the user lands directly on the OTP entry step instead of first seeing an email form on ePDS. - -### Flow 2: ePDS collects the email - -In Flow 2, your app just shows a "Sign in" button. Start OAuth normally and redirect the user to the authorization URL without `login_hint`. - -```ts -import { NodeOAuthClient } from '@atproto/oauth-client-node' - -const oauthClient = new NodeOAuthClient({ - clientMetadata: { - client_id: 'https://yourapp.example.com/client-metadata.json', - client_name: 'Your App', - client_uri: 'https://yourapp.example.com', - redirect_uris: ['https://yourapp.example.com/api/oauth/callback'], - scope: 'atproto transition:generic', - grant_types: ['authorization_code', 'refresh_token'], - response_types: ['code'], - token_endpoint_auth_method: 'none', - dpop_bound_access_tokens: true, - }, - stateStore, - sessionStore, -}) - -const url = await oauthClient.authorize('alice.certified.one', { - scope: 'atproto transition:generic', -}) - -const authUrl = new URL(url) -authUrl.searchParams.set('epds_handle_mode', 'picker') - -return authUrl.toString() -``` - -Without `login_hint`, ePDS renders its own email form and takes the user through the rest of the OTP flow. - -### Callback handling - -Callback handling stays standard. Once the user finishes on ePDS, your callback handler receives a normal authorization code and hands it back to `oauth-client-node`. - -```ts -const result = await oauthClient.callback(params) - -const session = result.session -const did = session.did -``` - -## Handle modes - -Handle mode controls what happens when a brand new user needs a handle during signup. - -| Mode | Behavior | -|------|----------| -| `picker-with-random` | Show the handle picker with a "Generate random" option. | -| `picker` | Show the handle picker without a random option. | -| `random` | Skip the picker and assign a random handle automatically. | - -Handle mode is resolved in this order: - -1. `epds_handle_mode` query param on the authorization URL -2. `epds_handle_mode` in client metadata -3. The ePDS instance default (`EPDS_DEFAULT_HANDLE_MODE`) - -This only affects new account creation. Existing users keep their current handle and skip this step. - -## Client metadata - -Your client metadata file is a public JSON document served over HTTPS. Its URL is also your `client_id`. - -### Bare-bones example - -```json -{ - "client_id": "https://yourapp.example.com/client-metadata.json", - "client_name": "Your App", - "client_uri": "https://yourapp.example.com", - "redirect_uris": ["https://yourapp.example.com/api/oauth/callback"], - "scope": "atproto transition:generic", - "grant_types": ["authorization_code", "refresh_token"], - "response_types": ["code"], - "token_endpoint_auth_method": "none", - "dpop_bound_access_tokens": true -} -``` - -### Full config example - -```json -{ - "client_id": "https://yourapp.example.com/client-metadata.json", - "client_name": "Your App", - "client_uri": "https://yourapp.example.com", - "logo_uri": "https://yourapp.example.com/logo.png", - "redirect_uris": ["https://yourapp.example.com/api/oauth/callback"], - "scope": "atproto transition:generic", - "grant_types": ["authorization_code", "refresh_token"], - "response_types": ["code"], - "token_endpoint_auth_method": "none", - "dpop_bound_access_tokens": true, - "brand_color": "#0f172a", - "background_color": "#ffffff", - "email_template_uri": "https://yourapp.example.com/email-template.html", - "email_subject_template": "{{code}} - Your {{app_name}} code", - "branding": { - "css": "body { background: #0f172a; color: #e2e8f0; }" - }, - "epds_handle_mode": "picker-with-random" -} -``` - -The extra branding fields customize the hosted login and email experience. `epds_handle_mode` sets your preferred handle mode for new users unless you override it on the authorization URL. - -## Branding and customization - -### How branding works - -ePDS reads branding settings from your app's `client-metadata.json`, using the OAuth `client_id` to look it up. Standard metadata fields like `logo_uri`, `brand_color`, `background_color`, `email_template_uri`, and `email_subject_template` customize the hosted login and email experience. - -Trusted clients can go further by adding custom CSS in client metadata under `branding.css`: - -```json -{ - "branding": { - "css": "body { background: #0f172a; color: #e2e8f0; }" - } -} -``` - -When the client is trusted, ePDS injects that CSS into its hosted auth pages and the stock consent page. - -{% callout type="warning" %} -Trust is checked against the exact `client_id`. - -The `client_id` you send during OAuth, the `client_id` inside `client-metadata.json`, and the entry in `PDS_OAUTH_TRUSTED_CLIENTS` must all be identical. - -For example, if your client metadata says `"client_id": "https://hypercerts-scaffold.vercel.app/client-metadata.json"`, then `PDS_OAUTH_TRUSTED_CLIENTS` must contain `https://hypercerts-scaffold.vercel.app/client-metadata.json` — not just `https://hypercerts-scaffold.vercel.app`. See the [Scaffold Starter App](/tools/scaffold) for a concrete example of a client serving metadata from `/client-metadata.json`. -{% /callout %} - -### Client metadata branding fields - -These fields are the main branding controls exposed through client metadata: - -| Field | What it affects | -|------|------------------| -| `logo_uri` | App logo shown in hosted auth and email flows | -| `brand_color` | Primary brand color used by hosted screens | -| `background_color` | Background color for hosted screens | -| `email_template_uri` | Custom HTML template for OTP emails | -| `email_subject_template` | Subject line template for OTP emails | -| `branding.css` | Custom CSS for trusted clients | - -### CSS injection for trusted clients - -Custom CSS is only applied for clients whose exact `client_id` appears in `PDS_OAUTH_TRUSTED_CLIENTS`. When present, ePDS injects a `` tag closure, and updates the page's CSP `style-src` directive with a SHA-256 hash for the injected stylesheet. - -This gives operators a safety boundary: untrusted clients never get CSS injection, even if their metadata contains branding CSS. - -### Where branding appears - -The send-OTP and initial-OTP screens are two states of the same auth-service route: `https://auth.epds1.test.certified.app/oauth/authorize`. - -| Surface | URL | Supports branding | -|---|---|---| -| Send OTP | `https://auth.epds1.test.certified.app/oauth/authorize` | Metadata fields + trusted-client CSS | -| Initial OTP | `https://auth.epds1.test.certified.app/oauth/authorize` | Metadata fields + trusted-client CSS | -| Choose handle | `https://auth.epds1.test.certified.app/auth/choose-handle` | Metadata fields + trusted-client CSS | -| Recovery | `https://auth.epds1.test.certified.app/auth/recover` | Metadata fields + trusted-client CSS | -| Consent page | `https://epds1.test.certified.app/oauth/authorize` | Trusted-client CSS | - -### Examples - -#### Send OTP - -{% columns %} -{% column %} -Stock - -![Stock send OTP screen](/images/epds/send-otp-stock.png) -{% /column %} -{% column %} -CSS injected - -![CSS-injected send OTP screen](/images/epds/send-otp-css-injected.png) -{% /column %} -{% /columns %} - -#### Initial OTP - -{% columns %} -{% column %} -Stock - -![Stock initial OTP screen](/images/epds/initial-otp-stock.png) -{% /column %} -{% column %} -CSS injected - -![CSS-injected initial OTP screen](/images/epds/initial-otp-css-injected.png) -{% /column %} -{% /columns %} - -#### Choose handle - -{% columns %} -{% column %} -Stock - -![Stock choose handle screen](/images/epds/choose-handle-stock.png) -{% /column %} -{% column %} -CSS injected - -![CSS-injected choose handle screen](/images/epds/choose-handle-css-injected.png) -{% /column %} -{% /columns %} - -#### Consent page - -{% columns %} -{% column %} -Stock - -![Stock consent page](/images/epds/consent-page-stock.png) -{% /column %} -{% column %} -CSS injected - -![CSS-injected consent page](/images/epds/consent-page-css-injected.png) -{% /column %} -{% /columns %} - -#### Recovery - -{% columns %} -{% column %} -Stock - -![Stock recovery screen](/images/epds/recovery-stock.png) -{% /column %} -{% column %} -CSS injected - -![CSS-injected recovery screen](/images/epds/recovery-css-injected.png) -{% /column %} -{% /columns %} - -## Further reading - -- [Account & Identity Setup](/architecture/account-and-identity) -- [Certified PDSs](/reference/certified-pdss) — the production, staging, and test ePDS instances Certified operates -- [Certified Group Service (CGS)](/architecture/certified-group-service) — a governance layer that sits in front of a PDS to support multi-identity, role-based repo management -- [Scaffold Starter App](/tools/scaffold) -- [ePDS repository](https://github.com/hypercerts-org/ePDS) -- Install the ePDS agent skill with `npx skills add hypercerts-org/ePDS --skill epds-login` +{% /remote-doc %} diff --git a/styles/globals.css b/styles/globals.css index 2ac886c..4c4c3c2 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -1068,6 +1068,11 @@ a.sidebar-link-active:hover { padding-left: 24px; } +.toc-link-h4 { + font-size: 12px; + padding-left: 40px; +} + /* ===== Last Updated ===== */ .page-tools { display: flex; @@ -1134,6 +1139,27 @@ a.sidebar-link-active:hover { font-style: italic; color: var(--color-text-secondary); } + +.remote-doc-status { + margin-bottom: var(--space-6); + padding: var(--space-3) var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: var(--color-bg-subtle); + color: var(--color-text-secondary); + font-size: 0.875rem; + line-height: 1.5; +} + +.remote-doc-status--error { + border-color: var(--color-warning); + background: var(--color-warning-bg); + color: var(--color-text-primary); +} + +.remote-doc-status--error strong { + color: var(--color-text-heading); +} /* ===== Pagination ===== */ .pagination { display: flex; @@ -1414,6 +1440,36 @@ a.sidebar-link-active:hover { color: #d6deeb; } +.mermaid-diagram { + margin: var(--space-6) 0; + padding: var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + background: var(--color-bg-subtle); + overflow-x: auto; +} + +.mermaid-diagram svg { + display: block; + max-width: 100%; + height: auto; + margin: 0 auto; +} + +.mermaid-diagram--loading { + color: var(--color-text-secondary); + font-size: 0.875rem; +} + +.mermaid-diagram--error { + border-color: var(--color-warning); + background: var(--color-warning-bg); +} + +.mermaid-diagram--error p { + margin-bottom: var(--space-4); +} + .layout-content code { font-family: var(--font-mono); font-size: 0.875em;