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

@flowget/templates

v0.1.5

Published

Builder-agnostic workflow-template picker for Flowget — a turnkey <WorkflowTemplates> modal (title + description cards) that loads a chosen template's graph onto the canvas, plus a headless useWorkflowTemplates() hook. Bring your own template source via t

Readme

@flowget/templates

Builder-agnostic workflow-template picker for Flowget.

When your builder opens a new workflow, <WorkflowTemplates> shows a modal listing workflow templates (title + description). Pick one and its graph loads onto the canvas — a fast path from a blank canvas to a working starting point.

No lock-in: the templates come from a swappable TemplatesAdapter (a hard-coded array, your BFF/DB over HTTP, or your own), and the picker never imports your builder — it reads and writes the canvas through the same currentGraph / applyGraph seam @flowget/ai-chat's <WorkflowChat> uses.

Two tiers

| Import | What | | --- | --- | | @flowget/templates | <WorkflowTemplates> — the modal + adapter loading + graph seam, batteries-included | | @flowget/templates | useWorkflowTemplates(…) — the headless hook (state + actions) to build your own UI | | @flowget/templates/styles.css | the stylesheet (import once) |

Also available as @flowget/templates/react (an alias of the default entry) for hosts that prefer an explicit client subpath.

Install

npm install @flowget/templates

react and react-dom are peer deps (you provide them). There is no other runtime dependency — the modal is self-contained (a portal + CSS).

Mount the picker

Drop <WorkflowTemplates> inside your builder's children slot. It's builder-agnostic: pass the live currentGraph, an applyGraph callback (e.g. your store's setGraph), and an isNew signal for "new workflow" mode.

import "@flowget/templates/styles.css";
import { WorkflowTemplates, memoryTemplatesAdapter } from "@flowget/templates";
import { useFlowgetWorkflow } from "@flowget/builder"; // your builder

const adapter = memoryTemplatesAdapter([
  {
    id: "notify-on-event",
    title: "Notify on event",
    description: "Wait for an event, then send a notification.",
    tags: ["events"],
    isNew: true, // → renders a "NEW" chip on the card
    updatedAt: "2026-07-10T14:30:00.000Z",
    // `workflow` is the pure workflow.json (nodes + edges) — the customer never
    // adds template metadata into it.
    workflow: {
      nodes: [
        { id: "on_event", type: "event_trigger", data: { eventName: "order_placed" }, position: { x: 60, y: 160 } },
        { id: "notify", type: "log", data: { message: "Order received", level: "info" }, position: { x: 340, y: 160 } },
      ],
      edges: [{ id: "e1", source: "on_event", target: "notify" }],
    },
  },
]);

function WorkflowTemplatesGate() {
  const { workflowId, graph, setGraph } = useFlowgetWorkflow();
  return (
    <WorkflowTemplates
      adapter={adapter}
      currentGraph={graph}
      applyGraph={setGraph}
      isNew={workflowId == null} // open the modal for a new/unsaved workflow
    />
  );
}

// inside your builder's children slot:
// <FlowgetBuilder ...><WorkflowTemplatesGate /></FlowgetBuilder>

WorkflowGraph's nodes / edges are readonly, so your applyGraph / setGraph must accept a readonly graph. The first-party @flowget/builder setter does (its PositionOptionalGraph setter takes a readonly graph), so the write path is safe without a cast.

WorkflowTemplates props:

| Prop | Type | | | --- | --- | --- | | adapter | TemplatesAdapter | the templates source (required) — see Adapters | | currentGraph? | WorkflowGraph | live canvas graph — read to detect "new/empty" mode | | applyGraph? | (g: WorkflowGraph) => void | commit a chosen template's graph to the canvas | | isNew? | boolean | explicit "new mode" signal (e.g. workflowId == null); wins over the graph heuristic | | open? | boolean | controlled open state (the host owns open/close) | | autoOpenOnNew? | boolean | auto-open when "new" mode begins (default true) | | closeOnLoad? | boolean | close the modal after a template loads (default true) | | showSearch? | boolean | show the search box + tag chips (default true) | | pageSize? | number | templates shown per page in the modal (default 4) — see Pagination | | searchDebounceMs? | number | debounce before a typed query runs adapter.search (default 200) | | compare? | (a, b) => number | turnkey-only override of the default grid's order (the adapter owns order otherwise) | | heading? / subtitle? | string | modal heading / subtitle | | searchPlaceholder? | string | search input placeholder | | emptyLabel? / noMatchLabel? / loadingLabel? | string | empty / no-match / loading text | | className? | string | extra class on the overlay (compose your own theme scope) | | theme? | string | stamp data-theme on the portal overlay so a scoped [data-theme] token override reaches the body-portaled modal (see Theming) | | renderCard? | slot | override one card's rendering (render fields as text — never dangerouslySetInnerHTML) | | renderEmpty? | slot | override the empty state | | renderModal? | slot | override the whole modal interior (keeps the hook's state + choose) | | onOpen / onSelect / onLoad / onClose / onError | callbacks | lifecycle hooks — see Callbacks |

"New mode" detection

The picker opens when the workflow is new. isNew is resolved as:

isNew = props.isNew ?? isGraphNew(currentGraph)   // isGraphNew → no nodes / no graph

Pass isNew explicitly whenever your host seeds new drafts with a starter graph (so the canvas is non-empty even when "new") — workflowId == null is the usual signal. If you don't pass it, the picker falls back to the empty-graph heuristic. Auto-open fires once per "new" episode, so dismissing the modal doesn't nag the user. It re-arms only on a genuine new-workflow transition — your explicit isNew signal leaving and re-entering "new" mode — never on transient graph emptiness. So once you've dismissed the picker, a throwaway add-then-delete that momentarily re-empties the canvas won't re-open it. (In pure heuristic mode, without an explicit isNew, the picker stays disarmed after its first open until the host remounts on a real new workflow.)

Adapters — the templates source

The key seam. <WorkflowTemplates> asks the adapter for its templates, search results, and tags — and never cares where they came from. All three methods may be sync or async.

type TemplatesAdapter = {
  list(): Template[] | Promise<Template[]>;
  search(query: { text?: string; tag?: string }): Template[] | Promise<Template[]>;
  listTags(): string[] | Promise<string[]>;
};

type Template = {
  id: string;
  title: string;
  description?: string;
  workflow: WorkflowGraph;    // the PURE workflow.json — nodes + edges only
  tags?: readonly string[];
  updatedAt?: string;         // ISO timestamp — shown subtly (the adapter sorts on it)
  isNew?: boolean;            // → renders a "NEW" chip on the card
};

A Template is a flat DTO wrapping the pure workflow (exactly the workflow.json shape — @flowget/types' WorkflowGraph has no template metadata of its own). The customer never edits the workflow.json; presentation lives in the sibling fields. Loading a template = applyGraph(template.workflow). Two adapters ship; a customer can write their own.

⚠️ Trust boundary. workflow is customer/DB-authored and is applied to the canvas verbatim — this package does not validate or sanitize it. Your applyGraph must validate/authorize the graph (against the workflow schema and your node catalog — reject unknown node types / malformed data) before it reaches the canvas. Same boundary @flowget/ai-chat documents for its LLM proposal. And when you customize a card (renderCard), render template fields as text — never pass them to dangerouslySetInnerHTML.

Ordering — the adapter owns it

The picker renders the adapter's returned array in order and never re-sorts. So a custom async/DB adapter keeps its own order (e.g. a relevance ranking), and headless consumers see the same order as the turnkey shell. The shipped memoryTemplatesAdapter sorts newest-first by updatedAt (missing timestamps last) so the default UX is unchanged. An empty search({}) — no text, no tag — is equivalent to list(); list is kept as the explicit call. To reorder the default grid without touching the adapter, pass a turnkey-only compare?: (a, b) => number prop.

In-memory — the batteries-included default. Define the templates as a variable and hand them over — you get search (title/description/tags text + exact tag) and listTags (deduped union of tags) for free:

import { memoryTemplatesAdapter } from "@flowget/templates";
const adapter = memoryTemplatesAdapter([ /* Template[] */ ]);

HTTP — a thin worked example of sourcing from your own BFF/DB (the async case). list/search hit url (search appends ?q=&tag=); listTags hits `${url}/tags` (override via tagsUrl):

import { httpTemplatesAdapter } from "@flowget/templates";
const adapter = httpTemplatesAdapter({ url: "/api/workflow-templates" });
//   GET /api/workflow-templates
//   GET /api/workflow-templates?q=notify&tag=events
//   GET /api/workflow-templates/tags
// The server owns the order — results render as returned (the picker never re-sorts).
// default body shapes: `Template[]` or `{ templates }`; `string[]` or `{ tags }` (tags deduped).
// Note: the default tagsUrl is `${url}/tags` (a plain suffix). If `url` carries a
// query string, pass `tagsUrl` explicitly (e.g. `tagsUrl: "/api/workflow-templates/tags"`).
// Override the mappings for any other shape:
const adapter2 = httpTemplatesAdapter({
  url: "/api/workflow-templates",
  map: (body) => body.data.map(fromRow),
  mapTags: (body) => body.tags.map((t) => t.name),
});

Your own — anything implementing the three methods:

const adapter: TemplatesAdapter = {
  list: async () => (await db.templates.findMany()).map(toTemplate),
  search: async ({ text, tag }) => (await db.search(text, tag)).map(toTemplate),
  listTags: async () => db.distinctTags(),
};

Search + tags

When showSearch is on (default), the modal renders a debounced search box (free-text → adapter.search({ text })) and selectable tag chips from adapter.listTags() (a tag click → adapter.search({ tag })). Text + tag compose into one query. Drive them yourself from the hook via searchText / selectedTag / tags + setSearchText / setSelectedTag.

Pagination

The modal paginates its grid — 4 templates per page by default — so a large template set never overflows the modal into a broken layout. Prev / Next controls and a "Page X of Y" indicator appear below the grid, and only when the current list spans more than one page (≤ pageSize templates → no controls).

Pagination is purely a display concern: a client-side slice of the array the adapter already returned. The TemplatesAdapter seam is untouched — list() / search() return the full set and are never asked to paginate (your service returns everything; it's not heavy). It applies to the currently filtered/searched list, and resets to page 1 whenever the search text or selected tag changes so a narrower result set never strands you past its last page.

Tune the page size with the additive pageSize prop:

<WorkflowTemplates adapter={adapter} isNew pageSize={6} />

Headless consumers get pagination too — the hook exposes pageItems (the current page's slice), page, pageCount, pageSize, and setPage(page) (clamped to [1, pageCount]) alongside the full templates set:

const { pageItems, page, pageCount, setPage } = useWorkflowTemplates({
  adapter,
  isNew: workflowId == null,
  pageSize: 6, // optional — defaults to 4
});

// render `pageItems` (not `templates`) for a paged grid, then:
<button disabled={page <= 1} onClick={() => setPage(page - 1)}>Prev</button>
<span>Page {page} of {pageCount}</span>
<button disabled={page >= pageCount} onClick={() => setPage(page + 1)}>Next</button>

Headless mode

<WorkflowTemplates> is a thin default shell over useWorkflowTemplates. To build a fully custom UI, drive the hook yourself:

import { useWorkflowTemplates } from "@flowget/templates";

function MyTemplatePicker() {
  const { graph, setGraph, workflowId } = useFlowgetWorkflow();
  const { templates, loading, open, close, select, load } = useWorkflowTemplates({
    adapter,
    currentGraph: graph,
    applyGraph: setGraph,
    isNew: workflowId == null,
  });

  if (!open) return null;
  return (
    <MyModal onClose={close}>
      {loading ? <Spinner /> : templates.map((t) => (
        <button key={t.id} onClick={() => { select(t); load(t); close(); }}>
          {t.title}
        </button>
      ))}
    </MyModal>
  );
}

The hook returns state templates, loading, error, open, isNew, selected, searchText, selectedTag, tags, the pagination view pageItems / page / pageCount / pageSize (see Pagination), and the actions setOpen / openModal / close / select / load / setSearchText (debounced) / setSelectedTag / setPage / reload. Templates

  • tags load lazily when the picker first opens; reload() re-fetches.

Lifecycle callbacks

Attach to either the component or the hook:

| Callback | Fires when | | --- | --- | | onOpen() | the modal opens (auto on "new", or via openModal) | | onSelect(template) | a template is highlighted (select) | | onLoad(template) | a template's graph was applied to the canvas (load) | | onClose() | the modal closes (dismissed, or after a load) | | onError(error) | loading templates from the adapter failed |

Theming

Import @flowget/templates/styles.css once. Every color / spacing / typography value reads a --flowget-* design token with a sensible built-in fallback, so the picker renders correctly on its own and inherits a host's theme wherever those tokens are defined (the same token contract as @flowget/ai-chat):

--flowget-color-accent, --flowget-color-chip-new, --flowget-color-bg-elevated,
--flowget-color-surface, --flowget-color-border, --flowget-color-text,
--flowget-color-text-subtle, --flowget-color-danger,
--flowget-radius-lg / -md / -sm, --flowget-shadow-lg / -md,
--flowget-font-sans, --flowget-font-size-*

Two package-owned knobs tune presentation without touching the token contract:

--fg-templates-z            /* overlay stacking order (default 300) */
--fg-templates-overlay-bg   /* backdrop scrim fill (default rgba(0, 0, 0, 0.5), a neutral dim) */

--fg-templates-overlay-bg is deliberately not a themed --flowget-* token — the scrim must read as a neutral dim over any host theme, so it is a package-owned knob you can override outright (e.g. --fg-templates-overlay-bg: rgba(0, 0, 0, 0.7)).

The "NEW" chip on template cards reads a dedicated --flowget-color-chip-new design token (fallback #22c55e, green). Overriding it recolors the templates chip. @flowget/builder's catalog NEW chip is being aligned to read this same public token (builder 0.5.1+), so once you're on a builder release that reads it, that single override recolors both chips — no double-customization:

/* recolors the templates NEW chip — and, on @flowget/builder 0.5.1+, the builder's catalog NEW chip */
--flowget-color-chip-new: #f59e0b;

All class names are namespaced fg-templates-*; override any of them, or swap the renderCard / renderEmpty / renderModal slots, for deeper changes.

⚠️ Scoped theme overrides and the body portal

The modal portals into document.body — outside your builder wrapper. So a token override you scope to a selector on that wrapper doesn't cascade in. If you theme the builder with a scoped block like

[data-theme="midnight"] { --flowget-color-accent: #22d3ee; /* … */ }

(the same block <FlowgetBuilder theme="midnight"> stamps data-theme for), those tokens fall back to :root inside the portaled modal unless you pass the matching theme:

<WorkflowTemplates adapter={adapter} isNew theme="midnight" />

theme stamps data-theme="midnight" on the overlay root, so the scoped block now matches and the tokens cascade into the modal. It's a plain string (builder-agnostic — not tied to any named-theme enum) and is omitted entirely when unset, so unscoped (:root-defined) tokens keep working with no change.

License

FSL-1.1-ALv2 — see LICENSE.