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/builder

v0.8.3

Published

Composable React workflow builder for Flowget — Provider + compound subcomponents, token-driven theming, replaceable primitives, slot-based customization, and position-optional graph loading (autoLayout / mergeGraph via dagre) for code-generated and AI-au

Readme

@flowget/builder

Composable React workflow builder for Flowget — Provider + compound subcomponents, token-driven theming, replaceable primitives, slot-based customization.

Alpha (0.1.0-alpha.0). See CHANGELOG.md for phase-by-phase history of what's shipped. The README below documents the currently shipped surface only — no future-promised features mixed in.

Full documentation

The README is the quick-reference. Long-form docs live under docs/:

Install

npm install @flowget/builder @xyflow/react react react-dom
# or: bun add / pnpm add / yarn add — package is PM-agnostic

Peer dependencies (you bring them):

  • react ^18.3 || ^19
  • react-dom ^18.3 || ^19
  • @xyflow/react ^12.0v11 is not supported. The builder is built on @xyflow/react@12; v11 has a different node/edge data shape and connection API.

Internal state is powered by zustand and bundled into the published artifact — you do not need to install zustand yourself. The bundled instance has private module identity, so your app's zustand stores stay isolated from the builder's.

Quick start

// app/workflows/[id]/page.tsx
import { FlowgetBuilder } from "@flowget/builder";
import "@flowget/builder/styles.css";
import { storage, execution, catalog } from "./adapters";

export default function Page({ params }: { params: { id: string } }) {
  return (
    <FlowgetBuilder
      workflowId={params.id}
      storage={storage}
      execution={execution}
      catalog={catalog}
    />
  );
}

The default <FlowgetBuilder> composes Provider + Toolbar + ActiveRunBanner + Catalog + Canvas + Inspector + HitlSlot.

Adapter optionality

Only storage is required. Omit any of the others to switch off the corresponding surface:

// Embeddable read-only past-run viewer — no execute, no edit, schema-evolution-resistant
<FlowgetBuilder workflowId={pastRunId} storage={storage} />

| Adapter | Optionality | Effect when omitted | |---|---|---| | storage | required | (can't render without a workflow to load) | | execution | optional | Toolbar drops Run / Cancel / status pill; ActiveRunBanner becomes inert | | catalog | optional | Unknown node types render via a raw-shape fallback (type id + data snapshot, no field-aware UI). Catalog sidebar shows a "no catalog provided" state. Lets you render past workflows whose node types have evolved since save. | | hitl | optional | HITL surfaces silently absent; HITL signals (if any) are no-ops |

Conditional adapters under exactOptionalPropertyTypes: true (the recommended TS setting) require the conditional-spread shape:

<FlowgetBuilder
  storage={storage}
  {...(includeExecution ? { execution } : {})}
  {...(includeCatalog ? { catalog } : {})}
/>

Passing execution={undefined} is rejected under strict optional-property semantics. Either omit the prop entirely or use the spread pattern above.

Compose your own layout when the default doesn't fit:

<FlowgetBuilder.Provider storage={storage} execution={execution} catalog={catalog}>
  <FlowgetBuilder.Toolbar />
  <main style={{ display: "flex", flex: 1 }}>
    <FlowgetBuilder.Catalog />
    <FlowgetBuilder.Canvas />
    <FlowgetBuilder.Inspector />
  </main>
  <FlowgetBuilder.HitlSlot strategy="drawer" />
</FlowgetBuilder.Provider>

Compose your own Toolbar

The Toolbar accepts children; sub-primitives (SaveAction, RunAction, DebugAction, etc.) consume internal hooks for their state, so customer composition needs zero context plumbing:

<FlowgetBuilder.Toolbar>
  <FlowgetBuilder.Toolbar.Breadcrumb />
  <div className="fg-toolbar__actions">
    <FlowgetBuilder.Toolbar.SaveAction />
    <MyCustomButton />
    <FlowgetBuilder.Toolbar.DebugAction />
    <FlowgetBuilder.Toolbar.RunAction />
    <FlowgetBuilder.Toolbar.StatusPill />
  </div>
</FlowgetBuilder.Toolbar>

Default behavior (no children) renders the standard cluster. The same pattern applies to Catalog, Inspector, Canvas, and HitlSlot. Item-mapping renderers (e.g. <ValidationOverlay renderIssue={...} /> and the nodeRenderers / edgeRenderers / fieldRenderers registries) stay render-prop / registry — that's the architect-locked seam from CAR-447 §C.3.

Validation: the panel is opt-in, the run/save gate is always live

The builder ships graph validators (≥1 trigger, no orphan trigger, decorator allow-list, DAG, per-node config) plus the shared @flowget/graph-validation static layer (unknown node type, template reference existence/reachability, whole-string type-compat, and declarative field rules — required / coarse type / enum / min / max / pattern). validateGraph is a thin adapter that concatenates both, so the builder's pre-submit verdict equals the worker's pre-flight verdict by construction.

Two things are separate:

  • The visual panel is opt-in. <FlowgetBuilder> mounts neither <AutoValidate> nor <ValidationOverlay>, so an empty / trigger-less canvas shows no "needs a trigger" panel unless you wire the two primitives through children.
  • The run/save gate is always live. The toolbar <RunAction> / <SaveAction> and <RunTestDialog> derive a live validateGraph(graph, catalog) blocking-error count on every render — independent of whether <AutoValidate> is mounted. There is no dark-store window where an invalid graph slips through.
import { FlowgetBuilder, AutoValidate, ValidationOverlay } from "@flowget/builder";

<FlowgetBuilder storage={storage} catalog={catalog}>
  <AutoValidate />      {/* runs validateGraph on every mutation → store */}
  <ValidationOverlay /> {/* renders the resulting issues + honesty note, bottom-left */}
</FlowgetBuilder>;

validationMode — gate save too, or keep drafts saveable

<FlowgetBuilder storage={storage} catalog={catalog} validationMode="strict" /> // default
  • "strict" (default, founder decision) — gate BOTH save and run on an invalid workflow. A draft can never be persisted in a state the worker's pre-flight would reject.
  • "run-only" — gate run only; drafts stay saveable. Run is gated in both modes.

Honesty boundary

The static layer is coarse by design — it reasons only over node.json declarations (ConfigField.valueType / OutputField.type) on the FieldValueType lattice; it never sees a live zod schema. A "valid" verdict is therefore never WRONG but is not COMPLETE: refinements (.int(), .email(), .positive(), .literal(…)) stay enforced at run time by the worker. The <ValidationOverlay> panel states this to the customer.

  • <AutoValidate> (panel) and the shared static layer both require a catalog — a catalog-less builder is un-gated (unknown node types render via the raw-shape fallback).
  • Want a subset of rules, custom rules, or an i18n-aware surface? Omit <AutoValidate> and run your own effect against the exported validateMissingTrigger / validateGraph / etc., pushing results via the store's setValidationIssues. getRunDisabledReason / getSaveDisabledReason remain the pure helpers the toolbar <RunAction> / <SaveAction> consume.
  • <ValidationOverlay> is position: absolute, so mounting it at the end of the tree (under children) is visually identical to an inline placement.

Changed in 0.4.0: the panel was previously mounted by default. Changed in 0.8.0: validateGraph became a thin adapter over @flowget/graph-validation, and the run/save gate is now always live (default validationMode: "strict"). See the CHANGELOG.

Debug is opt-in

The Debug button (<Toolbar.DebugAction>) and its JSON-inspector dialog (<Toolbar.DebugDialog>) are a dev tool the builder does not surface by default — the default <Toolbar /> cluster renders neither. Turn them on with the debug prop. For the common default-composition case it's a one-liner:

<FlowgetBuilder storage={storage} debug />   {/* forwards to the internal <Toolbar debug /> */}

Composing your own toolbar? Pass debug to <Toolbar> directly:

<FlowgetBuilder.Provider storage={storage}>
  <FlowgetBuilder.Toolbar debug />   {/* mounts the Debug button + dialog */}
  {/* ...Catalog / Canvas / Inspector... */}
</FlowgetBuilder.Provider>
  • <FlowgetBuilder>'s debug is a pass-through to its internal <Toolbar debug />; <Toolbar>'s debug is the real gate. Both default to false.
  • debug only affects the default cluster. When you supply your own <Toolbar> children, mount <Toolbar.DebugAction> / <Toolbar.DebugDialog> yourself — the prop is ignored on the custom-children path.
  • Both primitives stay exported (Toolbar.DebugAction / Toolbar.DebugDialog + the top-level DebugAction / DebugDialog), so nothing is lost — only the default auto-mount is gated.

Changed in 0.5.0 (CAR-1120): the Debug button/dialog was previously mounted by default. See the CHANGELOG for the one-line migration.

Reading builder state from your components

import {
  useFlowgetBuilder,        // mega-hook — all slices at once
  useFlowgetSelection,      // { selectedNodeIds, primary, setSelection, addToSelection, ... }
  useFlowgetWorkflow,       // { graph, isDirty, addNode, updateNodeData, moveNode, ... }
  useFlowgetExecution,      // { status, events, beginRun, ... } | null  ← null if no ExecutionAdapter
  useFlowgetValidation,     // { issues, blockingErrors, warnings, setIssues }
  useFlowgetNodeStatus,     // per-node status derived from execution events (memoized)
  useFlowgetTheme,
  useFlowgetPrimitives,
} from "@flowget/builder";

The canonical Q1 use case — "row click in side panel → select corresponding node in canvas":

const { setSelection } = useFlowgetSelection();
// ...
<button onClick={() => setSelection([targetNodeId])}>Inspect</button>

useFlowgetNodeStatus(nodeId) returns the node's current run status ("idle" | "running" | "succeeded" | "failed" | "cancelled" | "skipped") derived from the execution event stream. Memoized on (nodeId, events) so the O(events) scan only runs when events arrive, not per render.

Per-node context menu (ui.menu)

Catalog authors declare per-node menu items via BuilderHints.ui.menu. Right-clicking the node opens the menu; selection dispatches the Provider's onNodeMenuAction:

// In the catalog
const myNode: NodeMetadata<BuilderHints> = {
  id: "http_request",
  category: "action",
  handles: [/* ... */],
  label: "HTTP Request",
  ui: {
    menu: [
      { type: "item", id: "copy_curl", label: "Copy as cURL" },
      { type: "item", id: "test_endpoint", label: "Send test request", disabledIfRunning: true },
      { type: "separator", id: "sep1" },
      { type: "item", id: "view_logs", label: "View recent logs" },
    ],
  },
};

// In the page
<FlowgetBuilder
  storage={storage}
  catalog={catalog}
  onNodeMenuAction={(itemId, nodeId, ctx) => {
    if (itemId === "copy_curl") copyCurlForNode(ctx);
    if (itemId === "test_endpoint") fireSingleNodeTest(ctx.nodeId);
    // ...
  }}
/>

NodeMenuContext carries { workflowId, nodeId, nodeType, data } — enough for routing decisions without re-querying the store.

Writing a custom node renderer

The nodeRenderers registry maps a node type to a component that renders that node on the canvas. Customer components accept React Flow's NodeProps directly — Flowget-specific data (catalog metadata, status, validation) reaches them via hooks:

import { type NodeProps } from "@xyflow/react";
import {
  useFlowgetNodeStatus,
  NodeHandles,
} from "@flowget/builder";

type SlackData = { channel?: string };

function SlackMessageCard({ id, type, data, selected }: NodeProps<SlackData>) {
  const status = useFlowgetNodeStatus(id);
  return (
    <>
      <NodeHandles typeId={type ?? ""} />
      <div className={`slack-card slack-card--${status}`} data-selected={selected}>
        <h3>Slack message</h3>
        <p>Channel: {data.channel ?? "(unset)"}</p>
      </div>
    </>
  );
}

<FlowgetBuilder
  storage={storage}
  catalog={catalog}
  nodeRenderers={{ slack_message: SlackMessageCard }}
/>

<NodeHandles typeId={...} /> reads the catalog's HandleSpec[] for the node type and renders the right RF <Handle> elements with the right positions + ids. Use it to avoid hand-positioning Handle elements (which can drift away from the catalog's declared shape). If you prefer to wire Handles yourself, import { Handle, Position } from "@xyflow/react" and render them directly — same pattern, different ergonomics.

Same approach for edgeRenderers (customer accepts RF's EdgeProps, uses BaseEdge / getBezierPath / etc. from @xyflow/react).

HITL — human-in-the-loop signals

Customer pushes a HITL signal onto the queue (typically from an adapter integration when an execution event indicates a node is awaiting human input):

import { useFlowgetHitl } from "@flowget/builder";

function MyHitlTrigger() {
  const hitl = useFlowgetHitl();
  return (
    <button onClick={() => hitl.pushSignal({
      key: "approve",
      nodeId: "n5",
      nodeType: "approval_step",
      request: { reason: "User exceeded daily limit" },
    })}>
      Simulate signal
    </button>
  );
}

<HitlSlot /> reads the first pending signal and delivers it to the customer's HitlSlot component (registered via <Provider hitl={MyHitlSlot}>). Four rendering strategies:

  • strategy="modal" (default) — wraps in the Dialog primitive
  • strategy="drawer" — wraps in a side panel
  • strategy="inline" — renders in-place, customer handles positioning
  • strategy="off" — skips the slot; customer mounts <HitlSlot.Renderer /> elsewhere in their tree

Customer's HitlSlot component receives { open, onClose, signal, onResolve } and decides the UI (form, chat, approval buttons, etc).

Toolbar dialogs

Default <Toolbar /> auto-mounts <SaveWorkflowDialog /> and <RunTestDialog /> as siblings of the toolbar row (<DebugDialog /> only when <Toolbar debug /> — see Debug is opt-in). They wire to existing store state (saveDialogOpen / debugDialogOpen / runTestDialogOpen) and use the customer-replaceable Dialog primitive. Customer composing their own Toolbar children takes responsibility for mounting their own (or omitting them):

<FlowgetBuilder.Toolbar>
  <FlowgetBuilder.Toolbar.Breadcrumb />
  <FlowgetBuilder.Toolbar.SaveAction />
  <MyCustomButton />
  <FlowgetBuilder.Toolbar.RunAction />
  {/* Mount the dialogs you want */}
  <FlowgetBuilder.Toolbar.SaveWorkflowDialog />
  <FlowgetBuilder.Toolbar.RunTestDialog />
  {/* Omit DebugDialog if not needed */}
</FlowgetBuilder.Toolbar>

Composing your own Inspector

The Inspector is a Hybrid C area-slot — empty children renders Header + Body + ExpandFieldDialog; pass children for full layout control:

<FlowgetBuilder.Inspector>
  <MyCustomHeader />
  <FlowgetBuilder.Inspector.Body />
  <FlowgetBuilder.Inspector.VariablesPanel />
  <MyCustomDocsLink />
  <FlowgetBuilder.Inspector.ExpandFieldDialog />
</FlowgetBuilder.Inspector>

For per-field control, render <Inspector.Field field={...} /> directly:

<FlowgetBuilder.Inspector>
  <FlowgetBuilder.Inspector.Header />
  <div className="my-grid">
    <FlowgetBuilder.Inspector.Field field={urlFieldDecl} />
    <FlowgetBuilder.Inspector.Field field={methodFieldDecl} />
  </div>
</FlowgetBuilder.Inspector>

For maximum control, use the hooks directly — your component manages everything:

import { useFlowgetInspector, useFlowgetField } from "@flowget/builder";

function UrlField() {
  const { value, setValue, field, issues } = useFlowgetField<string>("url");
  if (field === undefined) return null;
  return (
    <label>
      {field.label ?? "URL"}
      <input
        value={value ?? ""}
        onChange={(e) => setValue(e.target.value)}
        aria-invalid={issues.some(i => i.severity === "error")}
      />
    </label>
  );
}

Writing a custom field renderer

The fieldRenderers registry maps a FieldType (or custom:${string} escape hatch) to a component. Customer components accept FieldRendererProps<TValue> and render against your design system:

import type { FieldRendererProps } from "@flowget/builder";

function MyText({ field, value, onChange, disabled, issues }: FieldRendererProps<string>) {
  return (
    <div className="my-field">
      <label>{field.label ?? field.key}</label>
      <input
        value={value}
        onChange={(e) => onChange(e.target.value)}
        disabled={disabled}
      />
      {issues?.map(i => <p key={i.id} className={`alert alert-${i.severity}`}>{i.message}</p>)}
    </div>
  );
}

<FlowgetBuilder
  storage={storage}
  catalog={catalog}
  fieldRenderers={{
    text: MyText,
    "custom:colorpicker": MyColorPicker,
  }}
/>

Precedence chain (resolved internally by pickFieldRenderer):

  1. fieldRenderers["custom:xyz"] if the catalog declares customType: "xyz"
  2. defaultFieldRenderers["custom:xyz"] (rare — customer-provided defaults registered globally)
  3. fieldRenderers[field.type]
  4. defaultFieldRenderers[field.type]
  5. UnknownFieldFallback — renders raw JSON + "no renderer registered" message

Wrap-not-replace shape: customer imports the default and composes:

import { TextField, type FieldRendererProps } from "@flowget/builder";

function MyWrappedText(props: FieldRendererProps<string>) {
  return (
    <div className="my-field-wrapper">
      <TextField {...props} />
    </div>
  );
}

onInsertVariable is @experimental in the current version. Customer renderers receive onInsertVariable(token) on FieldRendererProps, but the focus-routing wiring that delivers VariablesPanel clicks to the right field lands in a future iteration. Calling onInsertVariable from a renderer today is a no-op; the contract shape is stable, only the wiring is missing. See the JSDoc on FieldRendererProps.onInsertVariable for the focused-field model planned next.

Composing your own catalog sidebar

The Catalog is a Hybrid C area-slot — empty children renders the default body (Search + grouped List); pass children to take full layout control:

<FlowgetBuilder.Catalog defaultCollapsed={false}>
  <MyCustomBranding />
  <FlowgetBuilder.Catalog.Search placeholder="Search nodes..." />
  <MyCustomDivider />
  <FlowgetBuilder.Catalog.List
    grouping="category"
    itemRenderer={(node) => (
      <li className="my-card" data-category={node.category}>
        <strong>{node.label ?? node.id}</strong>
        {node.description ? <span>{node.description}</span> : null}
      </li>
    )}
    emptyState={<p>Bring your own empty-state copy here.</p>}
    noResultsState={<p>Nothing matches your query.</p>}
  />
</FlowgetBuilder.Catalog>
  • grouping="category" (default) groups items by NodeCategory. Set to "none" for a flat list.
  • itemRenderer is the render-prop override per item — Hybrid C item-mapping pattern. Customer's renderer is fully responsible for the <li> (or whatever element) shape, including drag-out handling if needed (text/flowget-node-type payload).
  • defaultCollapsed initializes uncontrolled collapse state. Customer who needs persistence wraps <Catalog> and forwards collapsed through their own state machine.
  • hideCollapseToggle hides the built-in toggle button when customer drives collapse externally.

Default item shape (<Catalog.Item />) is composable too — drop the itemRenderer and the default item renderer handles drag-out + collapsed-mode automatically.

Switching workflows: remount the Provider

<FlowgetBuilder.Provider> holds the Zustand store across re-renders, but workflowId / initialGraph are baked at first mount — they don't re-initialize on prop change. To switch between workflows inside the same parent tree, give the Provider a React key:

<FlowgetBuilder
  key={currentWorkflowId}            // remounts the whole builder + store on workflow change
  workflowId={currentWorkflowId}
  storage={storage}
  catalog={catalog}
/>

Mid-mount adapter / handler swaps are picked up via React context — the store stays intact. Only the workflow seed and the per-mount selection / execution state need a full remount.

CSS

import "@flowget/builder/styles.css";   // complete visual contract (recommended)
// — or à la carte (partial — see note below) —
import "@flowget/builder/tokens.css";   // only design tokens
import "@flowget/builder/reset.css";    // only the scoped reset

styles.css is the complete visual contract — design tokens + scoped reset + builder component styles + the @xyflow/react base layer (pane cursor, edge geometry, viewport transforms, zoom/pan). It is self-contained: you do not need to import @xyflow/react/dist/style.css separately (doing so anyway is harmless — the rules are idempotent). The à-la-carte tokens.css / reset.css are partial — they do not carry the builder component styles or the React Flow base, so use them only if you're assembling the full sheet yourself.

Themes are token overrides. The builder ships a neutral default plus data-theme="flowget" (brand) and data-theme="light" (light variant) opt-ins. Define your own by setting data-theme="<name>" and shipping a CSS rule:

[data-theme="acme"] {
  --flowget-color-accent: #ff6b35;
  --flowget-color-bg: #0b0e14;
  /* …override any of the ~45 documented tokens — see src/styles/tokens.css for the full list */
}
<FlowgetBuilder theme="acme" />

All tokens live under the --flowget-* namespace and are defined at :root; theme overrides apply via a [data-theme="<name>"] selector wherever that attribute is set. (Because tokens are global rather than scoped to the builder subtree, portalled UI that renders outside .fg-builder-root — e.g. a context menu portaled to document.body — still needs data-theme stamped on its own portal root to pick up a scoped override.) The reset, by contrast, is scoped to .fg-builder-root — it will not fight your app's global CSS, Tailwind preflight, or your design system.

The catalog maturity-hint chips expose their colors as public tokens too — --flowget-color-chip-new / -beta / -soon / -text (defaults #22c55e / #f59e0b / #6b7280 / #ffffff), all :root-overridable. --flowget-color-chip-new is deliberately shared with @flowget/templates, whose template-card "New" chip reads the same token, so a single :root { --flowget-color-chip-new: … } override recolors the chip in both packages. (See maturity hints.)

Extension authors — @flowget/builder/internal

For third-party @flowget/<ext>-style packages, not end-users. If you're embedding the builder in your app, you never import this.

The builder's React contexts + accessor hooks (useBuilderStoreApi, adapters, theme, primitives, renderers, callbacks) are published at the bare specifier @flowget/builder/internal. Every entry of this package — including the Provider and the useFlowget* hooks — imports them from there, so the createContext calls live in one shipped module that a bundler resolves once.

If you publish a component package that renders inside a customer's <FlowgetBuilder.Provider> (a custom field renderer, node renderer, inspector panel, etc.) and needs builder state, import from @flowget/builder/internal and mark both @flowget/builder and @flowget/builder/internal as peerDependencies + bundler external — exactly how you'd externalize react. That guarantees your extension reads the same Provider state the host writes, instead of bundling its own (dead) context instance.

// your @flowget/my-extension package.json
"peerDependencies": {
  "@flowget/builder": "^0.1.0",
  "react": "^18 || ^19"
}
// your extension component
import { useBuilderStoreApi } from "@flowget/builder/internal";

Architecture

Compound-component pattern (Radix-style). Provider injects adapters + theme + primitives + renderers; named subcomponents read from the React context tree and from the per-mount Zustand store.

Per architect verdict on CAR-447 §C.3 (Hybrid C):

  • Area slots — Toolbar / Catalog / Canvas / Inspector / HitlSlot — accept children. Sub-primitives use hooks for state. No render-prop ceremony, no (ctx) => plumbing.
  • Item-mapping slotsrenderIssue on ValidationOverlay; nodeRenderers / edgeRenderers / fieldRenderers registries — kept as render-prop / registry (iteration boundary makes children syntactically awkward there).
  • PrimitivesDialog, Tooltip, Popover, ContextMenu, Toast — replaced wholesale via <Provider primitives={{ Dialog: MyDialog }}>. Defaults are minimal HTML + ARIA; bring your own Radix / Headless UI / custom set if you need real focus traps / positioning. Primitives are headless: Flowget does not inject classes or styles on them. Customer-supplied primitives carry their own styling — what you ship is exactly what renders. The default primitives use the .fg-dialog-* / .fg-tooltip / .fg-popover etc. classes you can target from your token overrides, but replacement primitives are on their own.

The full architecture spec is CAR-447.

Developing locally

bun install                # or npm / pnpm / yarn
bun run build              # tsup → dist/{index.js,index.cjs,index.d.{ts,cts}} + dist/*.css
bun run typecheck
bun run lint
bun run test

License

FSL-1.1-ALv2. See LICENSE.