npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@openparachute/surface-render

v0.2.0

Published

React rendering primitives for Parachute surfaces — markdown + wikilinks, auth'd vault media (image/audio) embeds, multi-format (csv/json/yaml/code/plain) renderers, a note-format dispatcher, and an MDX-safe-by-default view. Good defaults + per-surface ov

Downloads

131

Readme

@openparachute/surface-render

React rendering primitives for Parachute surfaces — the layer a custom surface otherwise copy-pastes out of Notes. Sibling to @openparachute/surface-client (which owns the framework-agnostic auth + data layer: OAuth, VaultClient, token storage, core types).

This package owns "render a note":

  • Markdown + wikilinks<MarkdownView> (react-markdown + GFM) with a per-surface [[wikilink]] resolver and link-component hook.
  • Auth'd vault media<VaultImage> / <VaultAudio> that fetch /api/storage/… blobs with the surface's authorization.
  • Multi-format renderers<CsvRenderer> (→ table), <JsonRenderer> (pretty), <YamlRenderer>, <CodeRenderer>, <PlainRenderer>.
  • A format dispatcher<NoteRenderer> picks the renderer from the note's path; formatForPath is exported standalone.
  • MDX<MdxView> renders .mdx as markdown by default (safe; no code execution), with an opt-in component-allowlist evaluation seam.

It ships primitives + good defaults + override hooks, not a turnkey app shell. The surface owns routing, chrome, and domain components; this package never bakes in a URL space, a router, or an opinionated layout.

Design: parachute.computer/design/2026-06-03-surface-client.md — decisions A–D. This package is Phase 3.

Install

npm add @openparachute/surface-render @openparachute/surface-client react react-dom react-markdown remark-gfm
# optional, for fenced-code-block coloring in markdown:
npm add rehype-highlight

react, react-dom, react-markdown, and remark-gfm are required peer dependencies (the package doesn't bundle React, and MarkdownView imports remark-gfm unconditionally). rehype-highlight is an optional peer (only needed if you pass it for fenced-code-block coloring).

Quick start

import { MarkdownView, type WikilinkResolver } from "@openparachute/surface-render/markdown";
import { vaultClientFetchBlob } from "@openparachute/surface-render/embed";
import { Link as RouterLink } from "react-router";

// 1. Your surface decides the URL space for wikilinks (decision D):
import { resolvedLink, unresolvedLink } from "@openparachute/surface-render/markdown";

const resolve: WikilinkResolver = (target) => {
  const id = myIndex.lookup(target);
  // Unresolved targets STILL navigate (create-on-navigate) — the common case.
  return id ? resolvedLink(`/n/${id}`) : unresolvedLink(`/n/${target}`);
};

// 2. Adapt your router's <Link> to the link-component shape:
const Link = ({ href, className, children }) => (
  <RouterLink to={href} className={className}>{children}</RouterLink>
);

// 3. Adapt your vault client to the fetch-blob fn (for auth'd media).
//    `vaultClientFetchBlob` is a plain fn — safe at module scope. Inside a
//    component, prefer the memoized `useVaultFetchBlob(client)` hook.
const fetchBlob = vaultClientFetchBlob(client); // client from surface-client

<MarkdownView content={note.content} resolve={resolve} linkComponent={Link} fetchBlob={fetchBlob} />;

Or dispatch by format with <NoteRenderer>:

import { NoteRenderer } from "@openparachute/surface-render/note";

<NoteRenderer note={note} resolve={resolve} linkComponent={Link} fetchBlob={fetchBlob} />;

The hooks (decisions C + D)

| Hook | Shape | Why it's a hook | |---|---|---| | resolve | (target) => { href; exists } \| null | The surface owns the URL space (/n/<id>, /entity/<slug>, …). See Resolving wikilinks for the null vs { exists: false } distinction. | | linkComponent | ComponentType<{ href; className?; children }> | The surface injects its router's <Link> without the shared layer importing a router. Defaults to a plain <a>. | | fetchBlob | (url) => Promise<Blob> | The surface supplies the auth. Use the useVaultFetchBlob(client) hook (works with notes-ui's fetchAttachmentBlob subclass or a base VaultClient via storageUrl + token) or a custom function. | | highlight | (code, lang) => string | One syntax-coloring hook for all code paths — code/json/yaml notes and fenced code inside markdown. Defaults to escape-only (inert, no dependency); pass a highlight.js-backed fn for coloring. | | components | react-markdown Components | Per-element overrides merged over the defaults (and over the built-in highlight code renderer). | | overrides | per-format renderer fns | <NoteRenderer> lets a surface swap any format's renderer (e.g. a JSON tree view). The override prop types are exported as named aliases so no as-casts are needed. |

Resolving wikilinks (null vs { exists: false })

A WikilinkResolver returns one of three things, and the choice between null and { exists: false } is the single most common source of confusion — they look interchangeable but render materially differently:

| Return value | Rendered as | Navigable? | |---------------------------|-------------------------------------------------------|------------| | { href, exists: true } | live link, class wikilink wikilink-resolved | ✅ yes | | { href, exists: false } | dashed "create-on-navigate" link, wikilink wikilink-unresolved | ✅ yes | | null | inert <span>, wikilink wikilink-unresolved, no anchor | ❌ no |

  • { exists: false } keeps a working link to a destination that doesn't exist yet — the canonical "click to create" affordance (notes-ui links an unresolved [[Foo]] to /n/Foo, where the note is created on navigate).
  • null drops the link entirely — the words render as styled text the reader can see but cannot click.

For most surfaces, "unresolved-but-still-linked" is what you want. Two tiny helpers make the intent obvious at the call site:

import { resolvedLink, unresolvedLink } from "@openparachute/surface-render/markdown";

const resolve: WikilinkResolver = (target) => {
  const id = index.lookup(target);
  return id ? resolvedLink(`/n/${id}`) : unresolvedLink(`/n/${target}`); // still navigates
};

Reach for null (re-exported as the named sentinel INERT) only when an unresolved target should genuinely have no destination:

import { INERT, resolvedLink } from "@openparachute/surface-render/markdown";
const resolve: WikilinkResolver = (t) => (index.has(t) ? resolvedLink(href(t)) : INERT);

⚠️ Trust boundary: the resolver owns the href it returns — validate the target and mint hrefs you control. Never echo a vault-authored target string straight back as the href (a javascript: URI could be injected). The plugin sets the href verbatim and does not sanitize it.

Fetching auth'd media

/api/storage/… images and audio need the surface's authorization. Adapt your vault client to a fetchBlob with the useVaultFetchBlob hook — no more hand-writing useMemo(() => vaultClientFetchBlob(client) ?? undefined, …):

import { useVaultFetchBlob } from "@openparachute/surface-render/embed";

function NoteBody({ note, client }) {
  const fetchBlob = useVaultFetchBlob(client); // memoized; undefined when signed out
  return <MarkdownView content={note.content} fetchBlob={fetchBlob} />;
}

It accepts any client exposing fetchAttachmentBlob (notes-ui's subclass) or storageUrl + getAccessToken (a base VaultClient), returns undefined when the client can't produce blobs, and is memoized on client so the function identity is stable (important — fetchBlob is an effect dependency downstream). The lower-level vaultClientFetchBlob(client) adapter is still exported for non-hook contexts.

Syntax highlighting (one hook, everywhere)

There is one highlight hook — (code, lang) => htmlString — and it now covers both code paths:

  1. whole code/json/yaml notes (the format renderers), and
  2. fenced code blocks inside markdown (```ts … ``` in a .md note).

Pass it once to <NoteRenderer> (or <MarkdownView>) and every code path colors consistently, emitting the same <pre><code class="hljs language-X"> markup so one stylesheet themes them all:

import { NoteRenderer } from "@openparachute/surface-render/note";
<NoteRenderer note={note} resolve={resolve} highlight={highlightAs} />;
// highlightAs is your highlight.js-backed (code, lang) => string

The default (omit highlight) is escape-only — inert, no coloring, no dependency.

⚠️ Don't combine highlight with rehypePlugins={[rehypeHighlight]}. They are two routes to the same fenced-code result; using both double-processes the markup. Pick one:

  • highlight (recommended) — shares the hook the format renderers use, no extra peer dependency, one mechanism for every code path.
  • rehypePlugins={[rehypeHighlight]} — the older path; needs the optional rehype-highlight peer and only colors markdown fences (not code notes).

When highlight is set, the built-in markdown code renderer activates; a components.code override you pass still wins over both.

Wikilinks vs embeds

remarkWikilinks handles only [[…]] links. Embeds (![[…]]) are not handled here: the Obsidian import rewrites ![[file]] embeds to standard markdown images ![](/api/storage/…), which the img override routes through <VaultImage> (auth'd blob). This keeps the renderer's embed handling and the importer's rewrite as two ends of one contract. A surface with raw, un-imported ![[…]] embeds can preprocess them into /api/storage/… images or <VaultAudio> before rendering.

MDX safety (decision B)

<MdxView> renders .mdx as markdown by default — JSX expressions and component tags are inert, never executed. Arbitrary vault MDX cannot run code.

Opting into live MDX is the surface's explicit trust decision: pass an evaluate runtime (e.g. backed by @mdx-js/mdx) plus a mdxComponents allowlist. Only then does MDX evaluate, and only the allowlisted components mount. This package never bundles an MDX runtime.

// safe default — renders as markdown, executes nothing:
<MdxView content={note.content} resolve={resolve} />

// opt-in — YOU are evaluating note content as code; only do this for vaults
// whose authorship you trust:
<MdxView content={note.content} evaluate={myMdxRuntime} mdxComponents={{ Chart }} />

Emitted CSS classes (complete contract)

The renderers emit a fixed, styleable class contract. This is the complete list — every class any renderer can put on the page. Style against these (override className / components to opt out of any). Most need a surface theme; the affordance classes (scroll/warning/error/loading/audio) have sane defaults in the optional baseline stylesheet.

| Class | Emitted by | On | Purpose | |---|---|---|---| | prose-note | MarkdownView, Csv/Json/Yaml/Code/Plain, MdxView (default className) | container <div> | The typographic prose container. Surface owns the theme. | | wikilink | remarkWikilinks | every wikilink <a> / inert <span> | Base wikilink class (always paired with one below). | | wikilink-resolved | remarkWikilinks | resolved wikilink <a> | Live link to an existing note. | | wikilink-unresolved | remarkWikilinks | unresolved <a> or inert <span> | Dashed "doesn't exist yet" affordance. | | hljs | CodeRenderer, highlighted markdown fences | <code> | highlight.js host class — theme via any highlight.js stylesheet. | | language-<id> | CodeRenderer, markdown fences | <code> | The code language (language-ts, language-json, …). | | csv-scroll | CsvRenderer | wrapper <div> | Horizontal-scroll container for wide tables. | | csv-warning | CsvRenderer | <p> | Malformed-CSV inline warning. | | vault-media-loading | VaultImage, VaultAudio | <span> | Shown while an auth'd blob fetch is in flight. | | vault-media-error | VaultImage, VaultAudio | <span> | Shown when the auth'd fetch fails (renders the message). | | vault-audio | VaultAudio | wrapper <span> | Audio-embed container. | | vault-audio-label | VaultAudio | <span> | Optional caption next to the control. |

Wikilink <a>/<span> also carry data attributes: data-wikilink-target (the raw [[target]] text) and data-wikilink-resolved ("true"/"false").

Optional baseline stylesheet

So you're not source-spelunking to discover unstyled scroll/warning/error/ loading/audio elements, the package ships an optional baseline stylesheet with sane neutral defaults for those affordance classes:

import "@openparachute/surface-render/styles.css";

Scope, on purpose:

  • It styles the affordance classes (csv-scroll, csv-warning, vault-media-loading, vault-media-error, vault-audio*, dashed wikilink-unresolved) and a minimal no-theme .hljs (display block + overflow).
  • It does not theme .prose-note typography — that's your design decision.
  • It does not ship a highlight.js color theme — import one yourself (e.g. import "highlight.js/styles/github-dark.css").

Every value uses a CSS custom property with a neutral fallback (--sr-warning-fg, --sr-error-fg, --sr-loading-bg, --sr-muted-fg, …) so you can retheme by setting vars instead of overriding rules.

Override prop types

<NoteRenderer overrides={…}> and <MarkdownView components={…}> accept per-format / per-element override functions. The override prop types are re-exported as named aliases from the main entry so you can annotate your override functions without as-casts or deep imports:

import type {
  WikilinkResolver,
  MarkdownViewProps,
  NoteRendererOverrides,
  MarkdownOverride,        // (props: MarkdownOverrideProps) => ReactNode
  MarkdownOverrideProps,   // === MarkdownViewProps
  CodeOverrideProps,       // { content; language; className?; highlight? }
  HighlightableOverrideProps, // json/yaml: { content; className?; highlight? }
  BasicFormatOverrideProps,   // csv/plain: { content; className? }
} from "@openparachute/surface-render";

const overrides: NoteRendererOverrides = {
  markdown: (props) => <MyMarkdown {...props} />, // props: MarkdownViewProps, no cast
  code: ({ content, language, highlight }) => <MyCode … />,
};

Subpath exports

@openparachute/surface-render            barrel (everything)
@openparachute/surface-render/markdown   MarkdownView, remarkWikilinks, resolver/link hooks, resolvedLink/unresolvedLink/INERT
@openparachute/surface-render/embed      VaultImage, VaultAudio, FetchBlob, vaultClientFetchBlob, useVaultFetchBlob
@openparachute/surface-render/formats    Csv/Json/Yaml/Code/Plain renderers, parseCsv, highlight hook
@openparachute/surface-render/note       NoteRenderer, formatForPath, override prop types
@openparachute/surface-render/mdx        MdxView
@openparachute/surface-render/styles.css optional baseline stylesheet (affordance defaults)

License

AGPL-3.0