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

@gridd/embed

v1.4.0

Published

Drop-in script that turns gridd YAML fenced code blocks into Gridd iframe embeds on any static HTML page, plus editable React embed and stylesheet-editor components

Readme

@gridd/embed

Drop-in script that turns ```gridd YAML fenced code blocks into live, read-only Gridd iframe embeds on any static HTML page.

Usage

CDN

Include js-yaml (for YAML blocks; plain JSON works without it), then the embed script. Point data-gridd-base-url at the Gridd app you want to render into.

<script src="https://cdn.jsdelivr.net/npm/js-yaml@4/dist/js-yaml.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@gridd/embed/gridd-embed.min.js"
        data-gridd-base-url="https://wonderful-smoke-03be4ab0f.5.azurestaticapps.net/embeddable.html"></script>

Inline

Copy gridd-embed.min.js (or the full gridd-embed.js) into a <script> tag in your page. Set the base URL via the global before the script runs:

<script>
  window.GRIDD_EMBED_BASE_URL = "https://wonderful-smoke-03be4ab0f.5.azurestaticapps.net/embeddable.html";
</script>
<script>/* contents of gridd-embed.min.js */</script>

npm

npm install @gridd/embed

Then copy or serve node_modules/@gridd/embed/gridd-embed.min.js as a static asset.

Configuration

| Attribute / global | Description | Default | |---|---|---| | data-gridd-base-url on the <script> tag | URL of embeddable.html | https://localhost:3000/embeddable.html | | data-gridd-style-uri on the <script> tag | URI of a shared Gridd stylesheet JSON | (none) | | window.GRIDD_EMBED_BASE_URL | Same as above, set before the script loads | — | | window.GRIDD_STYLE_URI | Same as above | — |

Embeds are always read-onlygridd_readonly=1 is appended to every iframe URL automatically.

Markup

Place a <pre><code class="language-gridd"> block anywhere in the page body. The script replaces it with an iframe on DOMContentLoaded.

<pre><code class="language-gridd">
cells: |
  | * | **Label** | **Value** |
  |---|-----------|-----------|
  | * | Revenue   | $4.2M     |
</code></pre>

JSON is also accepted without js-yaml:

<pre><code class="language-gridd">{"cols":["*","*"],"rows":["*"],...}</code></pre>

YAML format

cells: |          ← markdown table; first row = col weights, first col = row weights
  |      | 0.5   | 1.5          |
  |------|-------|--------------|
  | *    | **Header** | Body   |

borders:
  - index: 1        ← row/col index of the border line
    isVertical: false
    preset: level3  ← visual style defined in the stylesheet

cellStyles:
  templates:
    MyHeader:
      preset: Header
      weight: bold
      size: 14pt
      fontFamily: Arial
      color: "#FFFFFF"
      isInverted: true     ← dark background + light text
      textAlign: center
      verticalAlign: middle
      shape:
        type: RoundedRectangle
        color: "#1B4F72"   ← fill colour
        outline: none
        radius: 8
        margin: 4
        marginLeft: 4
    MyBody:
      preset: Body
      weight: normal
      size: 11pt
      fontFamily: Arial
      color: "#333333"
      textAlign: left
      verticalAlign: middle
      paddingLeft: 12
      shape:
        type: RoundedRectangle
        color: "#F4F6F7"
        outline: solid
        outlineColor: "#D5D8DC"
        outlineWeight: 1
        radius: 6
        margin: 4
        marginLeft: 4
  "0,0": { template: MyHeader }   ← row,col → template name
  "0,1": { template: MyHeader }
  "1,0": { template: MyBody }
  "1,1": { template: MyBody }

Cell content supports **bold** and <br> line breaks.

Full example

Save the snippet below as example.html, open it in a browser (internet access required for the CDN scripts and the Gridd app).

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Gridd embed example</title>
  <style>
    body { font-family: Arial, sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; }
    h1   { font-size: 1.4rem; margin-bottom: 8px; }
    p    { color: #555; margin-bottom: 20px; }
    /* make the iframe fill the container instead of a fixed 600px */
    iframe { width: 100%; height: 360px; border: 0; display: block; }
  </style>
</head>
<body>
  <h1>Strategic options</h1>
  <p>Three paths Meridian Wealth can take with its mass-affluent segment.</p>

  <pre><code class="language-gridd">
cells: |
  |     | 0.45             | 1.5                                                                        | 1.1                                            |
  |-----|------------------|----------------------------------------------------------------------------|------------------------------------------------|
  | 0.5 | **Option**       | **Description**                                                            | **Implication**                                |
  | *   | **Defend**       | Retain the segment with targeted improvements to pricing, UX, and coverage | Lower investment; incremental gains            |
  | *   | **Transform**    | Build a competitive hybrid digital offering for mass-affluent clients       | Higher investment; structural advantage        |
  | *   | **Exit & Refer** | Wind down the segment; establish structured referral partnerships           | Margin improvement; feeder pipeline risk       |

borders:
  - index: 1
    isVertical: false
    preset: level3
  - index: 1
    isVertical: true
    preset: level3
  - index: 2
    isVertical: true
    preset: level3

cellStyles:
  templates:
    TH:
      preset: Header
      weight: bold
      size: 12pt
      fontFamily: Arial
      color: "#FFFFFF"
      textAlign: left
      verticalAlign: middle
      isInverted: true
      paddingLeft: 10
      shape:
        type: RoundedRectangle
        color: "#1B4F72"
        outline: none
        radius: 0
        margin: 2
        marginLeft: 2
    Option:
      preset: SubHeader
      weight: bold
      size: 12pt
      fontFamily: Arial
      color: "#1A5276"
      textAlign: left
      verticalAlign: middle
      paddingLeft: 10
      shape:
        type: RoundedRectangle
        color: "#D6EAF8"
        outline: solid
        outlineColor: "#A9CCE3"
        outlineWeight: 1
        radius: 0
        margin: 2
        marginLeft: 2
    TD:
      preset: Body
      weight: normal
      size: 11pt
      fontFamily: Arial
      color: "#333333"
      textAlign: left
      verticalAlign: middle
      paddingLeft: 10
      shape:
        type: RoundedRectangle
        color: "#FFFFFF"
        outline: solid
        outlineColor: "#E5E8EB"
        outlineWeight: 1
        radius: 0
        margin: 2
        marginLeft: 2
  "0,0": { template: TH }
  "0,1": { template: TH }
  "0,2": { template: TH }
  "1,0": { template: Option }
  "1,1": { template: TD }
  "1,2": { template: TD }
  "2,0": { template: Option }
  "2,1": { template: TD }
  "2,2": { template: TD }
  "3,0": { template: Option }
  "3,1": { template: TD }
  "3,2": { template: TD }
  </code></pre>

  <script src="https://cdn.jsdelivr.net/npm/js-yaml@4/dist/js-yaml.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/@gridd/embed/gridd-embed.min.js"
          data-gridd-base-url="https://wonderful-smoke-03be4ab0f.5.azurestaticapps.net/embeddable.html"></script>
</body>
</html>

Editable inline mode (React)

The vanilla script above is always read-only. The @gridd/embed/react subpath adds an editable inline embed: a React component that loads a Gridd document into the app, lets the user edit it in place, and streams changes back to your app. This is the mode the vanilla script can't express — it builds the iframe URL as ?gridd_inline=1, appending gridd_readonly=1 when you pass readOnly or gridd_editable=1 otherwise.

Off-localhost editing is allowlisted. The Gridd app only honours gridd_editable=1 when the embedding page's origin is localhost (dev) or on the app's trusted-origins allowlist. From any other origin the embed falls back to read-only regardless of readOnly. Ask a Gridd maintainer to add your production origin to the allowlist before relying on editing in production.

react is an optional peer dependency (^18 || ^19); installing @gridd/embed purely for the vanilla script pulls in nothing extra.

import { GriddEmbed } from "@gridd/embed/react";

function Editor({ griddJson, onChange }) {
  return (
    <GriddEmbed
      baseUrl="https://localhost:3000/embeddable.html"
      document={griddJson}              // object (base64-encoded for you) or a base64 string
      readOnly={false}                  // omit/false → editable; true → read-only
      width="100%"
      height="100%"
      onDocumentChange={(doc) => onChange(doc)}  // parsed JSON, debounced ~400ms upstream
    />
  );
}

Props

| Prop | Type | Description | |---|---|---| | baseUrl | string | URL of embeddable.html to load. Its origin is the trust boundary (see Constraint B). | | document | object \| string | The Gridd document. An object is JSON-stringified and base64-encoded for you; a string is treated as the base64 payload verbatim. | | readOnly | boolean | Defaults to false (editable). true appends gridd_readonly=1. | | styleSrc | string? | URI of a shared Gridd stylesheet JSON (passed as gridd_style_uri). | | styleSheet | GriddStyleSheetData? | Controlled live stylesheet ({ selectedSheet, sheets }). On each identity change it is pushed into the iframe and applied immediately — use it to reflect edits from a stylesheet-editor panel live. Unlike document, this is not load-once. | | width / height | string \| number | Iframe dimensions. Default "100%". | | onDocumentChange | (doc) => void | Called with the parsed Gridd JSON whenever the user edits. This is the hook to hang persistence off. | | onSaveFile | (json, name) => Promise<void>? | Override the "save to file" action. When omitted, the browser's showSaveFilePicker is used. | | onReady | (info) => void? | Called once the app announces itself, before any edit can arrive. info.version is the app build in the iframe (null from an app older than the announce). Stamp it on the deck — see Versions. |

Imperative snapshot (ref)

GriddEmbed forwards a ref exposing requestSnapshot(), which pulls a static raster of the embed's current render as a self-contained data: URL. Use it to replace live cross-origin iframes with <img>s before printing/exporting a deck, so the printed page contains no iframes.

import { useRef } from "react";
import { GriddEmbed, type GriddEmbedHandle } from "@gridd/embed/react";

const ref = useRef<GriddEmbedHandle>(null);

// later, e.g. inside an export handler:
const { dataUrl, width, height } = await ref.current!.requestSnapshot({ scale: 2 });
// -> render <img src={dataUrl} style={{ width, height }} /> in the print view

<GriddEmbed ref={ref} baseUrl={BASE} document={doc} readOnly />

The same ref also exposes getEmbeddableVersion(), which returns the app build announced during the load handshake (or null before it completes). requestSnapshot(opts?) returns Promise<{ dataUrl, width, height, format }>. width/height are the logical (pre-scale) box, so the <img> lays out exactly where the iframe was — pass scale: 2 for a crisp 2× raster without changing pagination. It rejects if the iframe isn't ready, the grid errors, or it doesn't answer within the timeout (default 3 s), so one hung grid can never stall a whole export — wrap each call in Promise.allSettled and fall back to the live iframe for any grid that rejects.

| Option | Type | Default | Description | |---|---|---|---| | scale | number | 2 | DPR multiplier for the raster. | | format | "png" \| "svg" | "png" | Preferred format (the iframe currently always produces PNG). | | timeoutMs | number | 3000 | Reject if no response arrives within this window. |

Constraints

A — the document is loaded once, not controlled. document is read when the iframe asks for it at mount, through a ref. Re-rendering with a new document object does not reload the editor — this is deliberate, so a parent re-render can never clobber in-progress edits. The living document flows outward via onDocumentChange; treat document as an initial value. To load a different document into a fresh editor, remount the component with a changed React key.

B — the host is the strict side of the handshake. The iframe posts messages with target origin "*"; the component only trusts messages whose event.source is its own iframe and whose event.origin matches baseUrl's origin, and it always replies to that exact origin (never "*"). So baseUrl must be the real origin serving embeddable.html — a mismatch silently drops the handshake and the embed stays blank.

Offline / self-hosted app

By default the component fetches the Gridd app from a URL. A host that must work with no network at all — a desktop shell rendering decks offline — cannot do that: the iframe is simply blank. For those hosts, install the optional companion package @gridd/embeddable-dist, which contains the built app, and serve it yourself.

npm install @gridd/embed @gridd/embeddable-dist

@gridd/embeddable-dist is an optional peer. It is a separate package on purpose: a website or an Office add-in that loads the app from a URL should not download several megabytes of app bytes it will never execute.

// Electron main process (or any Node host)
const { embeddableDir, embeddableVersion, assertBuilt } = require("@gridd/embed/embeddable-path");

assertBuilt();                       // fails loudly at startup rather than rendering a blank iframe
const dir = embeddableDir();         // absolute path to embeddable.html + all its assets
const appVersion = embeddableVersion();

// Serve `dir` as static files on a loopback port, then point the component at it:
//   <GriddEmbed baseUrl={`http://127.0.0.1:${port}/embeddable.html`} ... />

Do not reach into node_modules/@gridd/embed/dist/... by hand. The layout is not part of the package's contract; embeddable-path is.

Why embeddable-path is Node-only

The subpath sits behind a "node" condition in the exports map, with every other condition resolved to null. A web bundler that tries to import it gets a resolution error at build time rather than silently pulling fs, path, and the app's bytes into a browser bundle. If webpack tells you "./embeddable-path" is not exported under the conditions [...], that is the guard working — move the import into your main/server process.

Serving it correctly

Every asset URL in the build is relative, resolved at runtime from the URL the script itself was loaded from. That is what lets the same directory be served from http://127.0.0.1:<random-port>/ or a custom scheme. Two consequences for the host:

  • Serve the directory at a path root, and serve embeddable.html for /. Do not rewrite asset paths, and do not inject a <base> tag.
  • Serve it from a different origin than your host page. Edits push back to you over postMessage, and that bridge is active only when the iframe is cross-origin to the host. Two loopback ports is the simplest way to get there; a same-origin iframe would render fine and quietly never save.

The build makes no runtime network requests — no web fonts, no analytics, no license or feature-flag check. This is not incidental: a Google Fonts fetch that fails offline changes text metrics, and a deck laid out on a fixed logical canvas then exports to a different PDF.

In the Gridd repo:

| Command | What it proves | |---|---| | npm run build:embeddable-dist | builds, then fails on a reintroduced beacon host, a remote @font-face, an absolute or root-relative asset URL, a hardcoded webpack public path, or a missing version.json | | npm run verify:offline | drives the real thing in Chrome from two loopback ports: the grid renders, it is editable, an edit pushes back to the host, and every asset came from disk | | npm run verify:online | the same protocol check against the dev server, so a packaging change that breaks the hosted app is caught in the same breath | | npm run serve:embeddable-dist | the manual pass — open the printed host URL with DevTools set to Offline |

Editable origins

The Gridd app decides whether an inline embed may be edited from the real postMessage handshake origin — never document.referrer, which a Referrer-Policy can empty or trim. The rule is:

| Host origin | gridd_editable=1 sent | Result | |---|---|---| | localhost / 127.0.0.1 / [::1], any port | not required | editable | | on the app's allowlist | required | editable | | anything else | — | read-only |

It fails closed: a missing, opaque ("null"), or unrecognised origin is never trusted. A host serving the app from a loopback port therefore gets an editable embed by rule, not by accident.

To add a production origin, either get it added to the app's built-in allowlist, or build the app with GRIDD_EXTRA_EDITABLE_ORIGINS="https://your.origin" (comma-separated for several). It is still an allowlist chosen at build time — a runtime origin cannot talk its way in.

Versions

Three version numbers are in play, and they are not the same thing:

| What | How to read it | What it tells you | |---|---|---| | This host package | EMBED_PACKAGE_VERSION from @gridd/embed/react, or window.GRIDD_EMBED_VERSION from the vanilla script | which protocol implementation is in your bundle | | The Gridd app in the iframe | onReady({ version }), ref.current.getEmbeddableVersion(), or window.GRIDD_APP_VERSION | which app build is rendering and writing documents | | The app on disk (offline hosts) | embeddableVersion() from @gridd/embed/embeddable-path | which build you are about to serve |

Show the app version in an About dialog and stamp it on the deck. Every document the app writes already carries a griddVersion field for the same reason: offline there is no server to reconcile a deck edited against one app build and reopened against another, so the build that wrote it has to travel with it.

Stylesheet editor (React)

GriddStyleSheetEditor embeds the Gridd stylesheet editor (?styleSheet=1) as a reusable panel you can drop into any editor. It hosts the same origin/security handshake as GriddEmbed (Constraint B), seeds the editor with a stylesheet on mount, streams debounced draft edits for live preview, and reports save / cancel back to you. Persistence is host-owned — the component never writes anything; you decide what save means.

import { GriddEmbed, GriddStyleSheetEditor, type GriddStyleSheetData } from "@gridd/embed/react";

function DeckEditor({ document, committedSheet, setCommittedSheet }) {
  // Session-only draft feeds the live preview; never persist it.
  const [draft, setDraft] = React.useState<GriddStyleSheetData | null>(null);
  const effectiveSheet = draft ?? committedSheet;

  return (
    <div style={{ display: "flex" }}>
      {/* Live preview: controlled styleSheet reflects the draft immediately. */}
      <GriddEmbed baseUrl={BASE} document={document} styleSheet={effectiveSheet} readOnly />

      {/* The editor panel. */}
      <GriddStyleSheetEditor
        baseUrl={BASE}
        variant="panel"
        styleSheet={committedSheet}      // controlled: pass the committed sheet, not the draft
        onDraftChange={setDraft}         // session-only preview state
        onSave={(sheet) => { setCommittedSheet(sheet); persist(sheet); setDraft(null); }}
        onCancel={() => setDraft(null)}  // re-push committed via the controlled prop
      />
    </div>
  );
}

Props

| Prop | Type | Description | |---|---|---| | baseUrl | string | URL of embeddable.html. Its origin is the trust boundary (Constraint B). | | styleSheet | GriddStyleSheetData | The stylesheet to edit ({ selectedSheet, sheets }). Controlled — pulled on mount, re-pushed on identity change. Pass the committed sheet, not the live draft. | | variant | "panel" | Layout variant passed to the app as variant. Default "panel" (compact chrome + sticky footer). | | styleSrc | string? | URI of a shared Gridd stylesheet JSON (passed as gridd_style_uri). | | width / height | string \| number | Iframe dimensions. Default "100%". | | onDraftChange | (sheet) => void | Debounced draft edits for live preview. Session-only — do not persist. | | onSave | (sheet) => void | User saved. Persist the sheet and close the panel. | | onCancel | () => void | User cancelled. Discard the draft and close. |

Dirtiness & cancel

A draft is dirty exactly when your host-side draft state is non-null (draft !== null). Do not try to infer dirtiness from the sheet's own draftSheets — the app sets abandoned keys to undefined rather than deleting them, so the keys survive and would read as permanently dirty. On cancel, re-push the committed sheet (via the controlled styleSheet prop, e.g. by clearing your draft state) so the editor and any preview snap back to the last saved state.