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

@graview/embed

v0.1.21

Published

Mount a declared Graview app into any element — the scene, the Graview, or the routed pages — without the Shell, sized and themed within the element.

Readme

@graview/embed

Mount a declared Graview app into any element — a paragraph of a docs page, a card on a dashboard, a preview in a builder — without the Shell, sized to the element, themed within it, under one app bar.

import { mount } from "@graview/embed";

const handle = mount(document.querySelector("#garden")!, {
  app,                        // defineApp(...)
  seed,                       // the graph to open with, or nothing
  face: "graview",            // "scene" | "graview" | "pages" | "picture" — one named lens, no chrome: stop "#view=<place>"; leave it out and the app opens on its home view, else the scene
  stop: "#focus=plot-1",      // the scene's view state, as its URL fragment
  principal: { kind: "human", id: "june", roles: ["coordinator"] },
  bar: true,                  // the app bar over every face (the default)
  switch: "words",            // the bar's Scene/Pages switch: its words where they fit (the default), or "icons" for its marks alone
});

handle.setPath("/plots");     // a place: the plots' list, at the element's width
handle.setPath("/places/overview"); // the scene, at its own address
handle.setStop("#overview=1&expand=kind:plot");
handle.unmount();

<Embed {...options} /> is the same thing as a React component.

The app bar

One bar stands over every face (FR-131). At the left, the app: its mark — the brand's logo, when the document has one — and its name, said once, as the page's heading (heading: 1 when the host's page is the app, 2, the default, inside an article, false when the host's own heading says it), and the way home. Right after it, the switch between the app's two faces (FR-137): "Scene" and "Pages", an icon and a word each, two buttons whose aria-pressed says which is drawn. The words are drawn where they fit and the icons alone where they do not — on a phone, or in a narrow box beside a long place name — and always their accessible names and their titles; switch: "icons" asks for the icons alone at every width, for a host whose box is small or whose own page already says what the two faces are ([data-testid="app-faces"] says what was asked in data-switch and what is drawn in data-switch-drawn). Scene draws the scene under the bar; Pages goes back to the page the reader was on. A declaration may call them something else (pages: { scene: "The farm", pages: "Lists" }); the scene's address is /places/overview whatever it is called. Then, on Pages, the place you are on as one control — its name and a chevron — that opens every place the app has (FR-138): the home first, then the Lists (one per kind) and the Pictures (each named lens, and how the kinds connect), each with its mark, a long name wrapped. Every place is two presses away, Escape gives the keyboard back to the control, and the bar is one row of 48 px however many places there are; on a phone the place control is the page's first line, under the bar. On the scene the same control names what the scene shows — "The whole thing", or the picture in view — and lists its pictures (FR-144): choosing one moves the scene's in.view, and its address under routing: "address", as any other way to a picture does. Where the bar has room after the name, the switch and the tools, the places themselves stand on the row as words in their order, the one you are on underlined and always among them, and the rest fold into "More" (FR-145); measured from the bar's own width, so a narrow box keeps the one control. A harness reaches any place the same way at every width: [data-testid="app-place-<key>"] (with its data-place-path) is one element in the embed — press it if it is visible, else press app-places-open ("More", or the one control) and then press it in app-places; app-place-current says where the reader is. At the right, three tools of one size: Find (a small box that says "Find…" and its shortcut, ⌘K on a Mac and Ctrl K elsewhere, and is drawn wide while it is used; a magnifier that opens the box over the row on a phone), the standing (a dot in the tone of the rules, a number only when one is broken, opening what is broken; its name says "Everything is in order" otherwise), and the person (an avatar whose menu holds who is signed in, the seats, the host's own actions and, for whoever keeps the app, the installation and the studio). bar: false draws none of it, for a host whose own page already says all of that.

THE BAR FITS ITS BOX. Every rule the bar is drawn by reads the bar's own width, never the screen's: an embed in a 650 px box on a 1440 desk is a narrow bar, not a desk's bar squeezed, and below 640 px of its own it is a phone's bar (the place on the first line under it, Find a magnifier, Find's list a sheet the bar's width under it). When the row is short, Find gives first, down to a box that still says "Find…"; then the place, down to 9em of its name (whole in its title); then the app's name, which wraps between its words onto a second line and never inside one, its whole name the way home's title. The bar stays one row of 48 px in any box from 480 px up.

A HOME VIEW IS THE FRONT PAGE (FR-136). An app with a home view — the declaration's views.home, or a worker view attached to "home" — opens on Pages at its home, full width under the bar, on a desk as on a phone, when the host names no face; under address routing it does so at the bare address whatever face the host names. Nothing floats over the scene. A declaration that names another first place (pages.first) opens there.

A VIEW OF ONE RECORD SITS ABOVE ITS FIELDS (FR-149). A worker view registered through views with cardinality: "one" is drawn above the record's own fields, which stay editable: on the scene the record drawn at full is the view and then its fields, on Pages the record's page is its heading, the view, then its facts and what can be done. Its manifest's replaces: "page" draws it alone instead. A view may ask the host to fill a field from the record it draws (data-prefill, FR-150): see @graview/guest.

What a page loads first

The frame — the element's region, its theme, the app bar, the provider — is on the page when mount returns. Each face is a chunk of its own, fetched the first time it is drawn: a page that opens on the pages never loads the scene, one that opens on the scene never loads the router, and the framework's own cards, rows and record pages come with whichever face draws them first. Until a face's chunk arrives its box stands empty (aria-busy), and handle.drawn() resolves once the face asked for is on the page — after mount, and after every change of face.

A host that knows its face before it mounts starts that chunk at once, beside its own requests, and an embed mounted once it is here draws it in the first commit:

import { mount, preload } from "@graview/embed";

const face = innerWidth < 768 ? "pages" : "graview";
const faceReady = preload(face);            // with no face named, every face
const app = await fetchAndCompile();        // the host's own round trips, meanwhile
await faceReady;
const handle = mount(root, { app, face });  // drawn in this commit

What this asks of the framework, and what it adds: the theme scopes to the element (themeBaseCss(scheme, brand, { scope }), and the blocks a view is drawn with, viewsCss, and the scene's own rules, sceneCss, drawn by the face that needs them beside it) rather than the document, every rule of it held inside that element, so nothing of the host's is restyled; the panes size against the picture's own box (cqh) rather than the viewport; the routed face runs on a memory router, so the host page's address is never touched (unless the host's page is the app: see below); the brand's fonts are fetched by the embed rather than assumed. The page's icon is the host's: only a host whose page is the app passes favicon: true to wear the brand's (FR-124), and an app that prefers dark is drawn dark until the host stamps a scheme of its own. The store is in memory and starts from the seed on every mount, unless the host hands it one.

When the page is the app: the address bar

A host whose whole page is the app — Graview Cloud's hosted app is one — hands the routed face the address bar, so a place, a record and the home can be linked, reloaded and shared:

const handle = mount(root, {
  app, store,
  face: innerWidth < 768 ? "pages" : "graview",  // where a bare address opens
  routing: "address",                            // the default is "memory"
  basePath: "/apps/a1/",                          // where the app is served; "/" by default
});

The host answers every address under the base with the same page. The address then says which face is drawn and where on it, as the whole-page Shell spells it:

| Address | Drawn | |---|---| | /apps/a1/places/the-board, /apps/a1/tasks/t1 | the routed face, at that page | | /apps/a1/places/overview#focus=t1, /apps/a1/places/overview#overview=1 | the scene, at that stop (the Graview at altitude) | | /apps/a1#focus=t1 | the scene too — a link written before the scene had an address — tidied on arrival to the scene's address | | /apps/a1 | the routed face's home; on arrival, the home when the app has a home view, else the host's face (the Graview or the scene land on the scene's address) |

Each page the routed face opens is pushed, and Back returns. The scene keeps its stop in the fragment the way the Shell does: a step is pushed, moving the furniture replaces. The switch and the place list push the address of where they go, the scene's included, so Back undoes it. A reload stays where it was. faceAtAddress(options) is the face an address opens on, for a host that renders <Embed> itself. placesOf(app) gives each place's address within the app, and addressOf(place, { basePath }) from @graview/core gives it under the base, spelled as the face's own links are (pathWithin reads one back).

routing: "memory", the default, is for somebody else's page: it never writes history and leaves location as it was. A host that keeps its own history stays on memory routing: onNavigate(path, how) is told each place the reader goes to (the path within the app — /places/overview for the scene — and "push", "replace" or "pop"), and handle.setPath(path) sends the reader back to one when the host's own Back arrives: the scene's path draws the scene, any other the page. where().path is the place's path the same way.

When the declaration changes

A chat that changes the app hands the host a new compiled app and a new store. handle.setApp(app, store) swaps them under the reader (or setApp(app, remote), whose presence comes with it), and the reader stays where they were:

remote.onDeclaration((next) => handle.setApp(latestApp, next));

The face, the place or record open on Pages, and the scene's stop and focus are kept. What the change took away falls back to its nearest parent: a removed record to its kind's list (or, in the scene, its kind's group); a removed kind or place to the home; a lens gone from the scene to its kind's group, or the home when the kind went too. Under address routing the address stays the source of truth, and one that names something gone is replaced, not pushed. The seat, the seats, the people, the scheme, the brand and the notices stay; the faces are drawn again, so an open menu, a scroll position and a half-typed field do not. drawn() resolves once the new app is on the page.

A renamed app says its new name at once (FR-128). A label that was the app's own name — as a host that mounts with label: app.name gives it — follows the new app: the embed's accessible name and each landmark inside say the new name, and the bar's name — its heading — is the new app's. A label the host chose ("Chapter 13") stays; setApp(app, store, { label }) gives another, and handle.setLabel(label) renames the embed in place. handle.setHostActions(actions) changes the host's own actions in the profile menu the same way.

A host that must remount reads the place first and hands it back:

const at = handle.where();           // { face, path, stop, kind? }
handle.unmount();
handle = mount(root, { app, store, at });

What stands over what

Every popover, menu and list of suggestions the embed draws — the profile, the problems, the districts a row could not hold, a card's acts at the pointer — opens in the browser's top layer, hung from what opened it and kept to the viewport, so nothing in the embed (the ask field, the altitude control, the scene) and nothing on the host's page stands over it. It is still inside the embed's element, so the scoped theme reaches it and nothing of it lands on the host. Everything that stays on screen takes a rung of one ladder, written once as custom properties on the embed's element: the scene, then the altitude control, then the rails and floating controls, then popovers, dialogs and notices, --graview-layer-scene through --graview-layer-toast. A host that lays something of its own over the embed reads the rung it means rather than guessing a number.

The host's own actions

A host's links about the app and the person — "Change the app", "Your apps", "Report this app" — go in the person's menu on the app bar, under who is signed in, rather than in a menu of the host's own laid over the scene:

mount(root, {
  app,
  hostActions: [
    { label: "Change the app", href: `/apps/${id}/change` },
    { label: "Your apps", href: "/apps" },
    { label: "Report this app", href: `/report?app=${slug}`, target: "_blank" },
    { label: "Sign out", onSelect: () => signOut() },   // a press rather than a link
  ],
});

Each is a stop for the keyboard in the menu, in the order given, drawn in the embed's own scheme; a press closes the menu. The whole-page Shell takes the same host actions.

The ask field

At the foot of both faces stands one quiet field, "Ask …". Asked, it grows into a panel over the app — never pushing the page — with a line about where the reader is, a few things to ask, at most three acts (the repairs a broken rule names for what they are on, and their own pins) and the conversation; ⇄ snaps it to the other foot, and on a phone it is a bottom sheet with a grab line to put it away. Escape closes it and puts the keyboard back. The conversation is the app's, so it is still there after a switch between the scene and Pages, and Find's last row, "Ask: ‘…’", asks it. The host can leave it out:

mount(root, { app, seat: "hidden" });   // "field" (the default) or "hidden"

What answers it is the host's to decide, once — a reader is never asked. The graph answers first, always: where things are, what is wrong, what a record says, the repairs a rule names, a view a template draws. Give the seat a model and open questions go to it:

mount(root, {
  app,
  ai: {
    complete: (prompt) => myModel(prompt),   // prompt in, text out
    name: "house-model",                     // logged as via "ai:house-model"; never shown
    // decide: jevDecide({ ... }),            // typed decisions, if you have a provider
    // onDevice: true,                        // a model in the reader's browser, off unless said
  },
});

With no ai, an open question is answered "I can answer about what's in this app. Open questions need AI, which isn't on here." An answer a model gave carries one quiet "Answered with AI"; one the graph gave carries nothing.

The host's notices

What the host has to say while the app is open — a newer version, the connection gone, the app held while a repair is checked, a conflict to settle, a refusal — it says in the app's own notices rather than in elements of its own fixed over the app:

const offline = handle.notify({ kind: "banner", sentence: "Offline — changes will be sent when you reconnect.", tone: "warn" });
offline.update({ sentence: "Back online.", tone: "good" });
offline.dismiss();

handle.notify({ kind: "toast", sentence: `The app was changed — now version ${version}` });
handle.notify({
  kind: "toast",
  sentence: conflict.sentence,
  tone: "warn",
  actions: [{ label: "Keep theirs", onSelect: conflict.keepTheirs }, { label: "Use mine", onSelect: conflict.useMine }],
});
handle.notify({ id: "newer", kind: "banner", sentence: "A newer version is available.", action: { label: "Reload", onSelect: () => location.reload() } });

A toast goes by itself after six seconds (timeout says otherwise, false keeps it), unless it carries an action, when it waits for one; a banner stays until it is dismissed. A notice said again under the same id takes the place of the one before. An action is a press (onSelect) or a link (href), and either closes the notice; every notice also has a dismiss control. Banners are drawn at the top of the picture and toasts at its foot, in the framework's floating panel and the embed's scheme, in the top layer and kept over any popover that opens after them; each is said aloud as it arrives, and one whose tone is bad is said as an alert. @graview/embed/pages has the same notify; a React host drawing <Embed> makes a board with createNoticeBoard(), passes it as notices, and says things on it.

What went wrong, and how long it took

mount(root, {
  app,
  onError: ({ name }, { module, face }) => beacon("embed-error", { name, module, face }),
  onReady: ({ ms, face }) => beacon("embed-ready", { ms, face }),
});

A view, a page, the bar or the studio that throws is contained where it threw: it says it could not draw and offers to try again, and the rest of the embed keeps working. The host is told the error's class (a TypeError, a GraphError) and the framework module that caught it, never the message, which may quote a record. The ready callback is told once, when the first face is drawn, how many milliseconds it took.

What the host can keep

The Studio is in the person's menu on the scene for the seat that keeps the app, and writes through a dev server's door or hands over files. A host whose readers cannot save a declaration leaves it off, or keeps what it applies:

mount(root, { app, studio: false });                      // no Studio place at all
mount(root, { app, studio: { onApply: ({ app, migration, files }) => propose(app) } });

Handed an onApply, the studio asks after no door and writes nothing; what the checker passed is the host's. A host that could not keep it answers ok: false with a sentence, which the studio says as its heading, and findings, which may be empty; an Apply with nothing changed is said by the studio and never reaches the host.

@graview/embed/pages is the routed face alone, with the same options less the face, the stop, the heading and the studio, and the same handle less the faces. It takes views, and its pages draw the same cards, rows and record pages the whole embed's would. A page that only ever shows the pages imports that and does not bundle the scene, the Graview or the studio:

import { mount } from "@graview/embed/pages";

Bundled for the browser without React, the pages face alone is about 500 KB minified (165 KB gzipped); the whole embed loads about 750 KB (190 KB) before a face is fetched, and every face about 1.28 MB (370 KB); node scripts/inspect-pack.mjs fails CI when one passes its budget (scripts/lib/bundle-budget.mjs).

In a chat's widget

An MCP Apps frame (a ChatGPT or Claude widget) may refuse storage, is sized from its content, and is told its theme by the chat. The embed holds there:

const remote = await openRemote({ app, url, principal });   // @graview/ship
const handle = mount(root, {
  app,
  remote,                       // the store, and who is here, from the server
  principal,                    // who signed in: the only seat a hosted reader has
  people,                       // [{ id, name, kind? }]: names for the rail, offering nobody a seat
  height: "auto",               // the pages face as tall as its page
  pagesBelow: 560,              // narrower than this, the scene gives way to the pages
  hostContext: { theme },       // the chat's theme, over the page's own
  memory,                       // where the reader's settings are kept, if not the page's storage
  onIntrinsicHeight: (height) => notify("ui/notifications/size-changed", { height }),
});
handle.setHostContext({ theme: "dark" });   // the chat changed theme
handle.setPeople(next);                     // an agent who first acted after the page opened

Nothing touches localStorage or sessionStorage without a fallback, so a frame that throws on either still mounts. scheme: "auto" (the default) follows the host context, then the page's data-theme stamp, then the system's preference, each as it changes. Presence a channel reports is dropped once its last word is older than REMOTE_PRESENCE_TTL_MS, whether or not the channel says the person left. A host that builds presence itself keys each participant with participantKey({ kind, id, session }) from @graview/core, the op log's own kind:id:session.