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

@unnoodle/browser

v0.1.4

Published

Unnoodle Browser SDK — embed AI context assistance into any web application

Readme

@unnoodle/browser

Embed AI context assistance into any web application.

npm version Bundle Size License: MIT


What is it?

@unnoodle/browser is a lightweight, framework-agnostic SDK that adds a floating AI assistant button to your web application. It automatically collects runtime context (current route, page title, environment) and sends it to the Unnoodle API to surface intelligent suggestions.

  • Zero React / framework dependency
  • Shadow DOM — completely isolated from your app's CSS
  • < 50kb minified
  • Works with Next.js, React, Vue, Svelte, or plain HTML

Installation

npm install @unnoodle/browser
# or
pnpm add @unnoodle/browser
# or
yarn add @unnoodle/browser

Quick Start

import { Unnoodle } from "@unnoodle/browser";

Unnoodle.init({
  apiKey: process.env.NEXT_PUBLIC_UNNOODLE_API_KEY,
  context: "engineering",
});

That's it. A floating button appears in the bottom-right corner. Click it or press ⌘U (macOS) / Ctrl+U (Windows/Linux) to open the panel.

Where is my API key? Log in to your workspace → Settings → API Key.


Full Configuration

A starter config file is included in the package as unnoodle.config.ts. Copy it into your project root and fill in your values.

import { Unnoodle } from "@unnoodle/browser";

Unnoodle.init({
  // Required: workspace API key (Settings → API Key in your workspace)
  apiKey: process.env.NEXT_PUBLIC_UNNOODLE_API_KEY,

  // Required: context slug to load — created automatically if new
  context: "engineering",

  // Optional: target environment (default: "prod")
  environment: "dev",

  // Optional: override API base URL (default: https://api.unnoodle.com)
  baseUrl: "http://localhost:8080",

  // Optional: provide an auth token at request time
  getAuthToken: () => localStorage.getItem("auth_token"),

  // Optional: the Unnoodle design system's look applies with nothing passed.
  // Match your product with a few brand roles (see "Theming" below).
  theme: {
    brand: { primary: "#0f766e", font: "inherit" },
  },

  // Optional: attach additional metadata to every request
  customMetadata: () => ({
    userId: window.__APP_USER_ID,
    plan: "enterprise",
  }),
});

Programmatic Control

import { Unnoodle } from "@unnoodle/browser";

// Open the panel manually (e.g. from your own help button)
Unnoodle.open();

// Close the panel
Unnoodle.close();

// Remove the widget entirely (useful in SPAs)
Unnoodle.destroy();

The individual named exports (openUnnoodle, closeUnnoodle, destroyUnnoodle) are still available for tree-shaking.


Framework Examples

React / Next.js (App Router)

Root layouts in Next.js App Router are Server Components and run on the server where document does not exist. You must call initUnnoodle() from a separate "use client" component rendered inside the layout body.

Step 1 — install the package

npm install @unnoodle/browser
# or
pnpm add @unnoodle/browser

Step 2 — create a thin client component (e.g. components/UnnoodleInit.tsx)

"use client";
import { useEffect } from "react";
import { initUnnoodle } from "@unnoodle/browser";

export function UnnoodleInit({
  apiKey,
  context,
}: {
  apiKey: string;
  context: string;
}) {
  useEffect(() => {
    initUnnoodle({ apiKey, context });
  }, [apiKey, context]);

  return null; // renders nothing — side-effect only
}

Step 3 — render it inside your layout body

// app/layout.tsx — stays a Server Component, no "use client" needed here
import { UnnoodleInit } from "../components/UnnoodleInit";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <UnnoodleInit
          apiKey={process.env.NEXT_PUBLIC_UNNOODLE_API_KEY!}
          context="engineering"
        />
        {children}
      </body>
    </html>
  );
}

Why not "use client" on the layout directly?
Next.js App Router layouts that render <html> and <head> must be Server Components. Adding "use client" would cause a build error. The thin wrapper component is the standard pattern recommended by the Next.js team.

React / Next.js (Pages Router)

// pages/_app.tsx
import { useEffect } from "react";
import { Unnoodle } from "@unnoodle/browser";

export default function App({ Component, pageProps }) {
  useEffect(() => {
    Unnoodle.init({
      apiKey: process.env.NEXT_PUBLIC_UNNOODLE_API_KEY,
      context: "engineering",
    });
  }, []);

  return <Component {...pageProps} />;
}

Vue 3

// main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { Unnoodle } from "@unnoodle/browser";

const app = createApp(App);
app.mount("#app");

Unnoodle.init({
  apiKey: process.env.VITE_UNNOODLE_API_KEY,
  context: "engineering",
});

Plain HTML / CDN

<script type="module">
  import { Unnoodle } from "https://cdn.unnoodle.com/browser/latest/index.js";
  Unnoodle.init({
    apiKey: "pk_live_xxxxx",
    context: "engineering",
  });
</script>

Plugin System (Advanced)

The SDK includes an internal plugin interface for advanced use cases. Plugins can intercept the context collection, response rendering, and error handling lifecycle.

import { initUnnoodle, type UnnoodlePlugin } from "@unnoodle/browser";

const analyticsPlugin: UnnoodlePlugin = {
  name: "analytics",
  onContextCollected: (ctx) => ({
    ...ctx,
    sessionId: getSessionId(),
  }),
  onResponse: (response) => {
    trackEvent("unnoodle_response", { hasActions: !!response.actions?.length });
  },
  onError: (err) => {
    reportError(err);
  },
};

Unnoodle.init({
  apiKey: process.env.NEXT_PUBLIC_UNNOODLE_API_KEY,
  context: "engineering",
  _plugins: [analyticsPlugin],
});

Note: _plugins is prefixed with _ to indicate it's an advanced internal API. It may be promoted to a stable API in a future major version.


API Reference

Unnoodle.init(config: UnnoodleConfig): void

Initializes and mounts the widget. Safe to call in SSR environments — no-ops if document is unavailable.

Also available as the named export initUnnoodle() for tree-shaking.

openUnnoodle(): void

Programmatically opens the panel.

closeUnnoodle(): void

Programmatically closes the panel.

destroyUnnoodle(): void

Tears down the widget, removes DOM elements, and cleans up all event listeners.


Theming

The Panel, rail and launcher button render in shadow roots with the Unnoodle design system's look — nothing to pass. To make the widget look like part of your product, pass your brand (design record: ADR-0016, docs/engineering/systems/flow-engine/decisions/0016-widget-theming.md):

initUnnoodle({
  apiKey: "pk_live_…",
  context: "engineering",
  theme: {
    brand: {
      primary: "var(--brand)",        // a reference to your own CSS variable follows it live
      primaryHover: "var(--brand-dark)",
      secondary: "#1f2e2b",           // the rail, launcher and Panel header
      surface: "var(--paper)",
      text: "var(--ink)",
      border: "var(--line)",
      font: "inherit",                // your page's body font
      radius: "8px",
    },
  },
});

| Role | Used for | If not passed | | --- | --- | --- | | primary | filled buttons, links, focus ring, selected states | design system blue | | primaryHover | hover on primary buttons | primary darkened 15% | | onPrimary | text and icons on primary | white, or ink when primary is a light colour | | secondary | the chrome: rail, launcher button, Panel header (colour or gradient) | charcoal gradient | | secondaryHover | hover and active items on the chrome | onSecondary at 10% | | onSecondary | icons and text on the chrome | white, or ink when secondary is a light colour | | accent | highlights: concept chips, term links, resize handle | primary | | surface | Panel background (lighter fills derived) | white | | text | main text (muted and subtle shades derived) | design system ink | | border | borders (subtle shade derived) | design system grey | | font | font family; "inherit" = your page's body font | Poppins → system sans-serif | | radius | base corner radius; smaller and larger radii derived | 6px |

Every value is a CSS value. Derived shades use color-mix() (Chrome 111, Safari 16.2, Firefox 113). The automatic white-or-ink choice works for literal colours (#rgb, #rrggbb, rgb()); for a var() or a gradient, pass onPrimary / onSecondary if white is wrong. Dark mode is partial: surface, text and border move most of the Panel; status colours stay light-theme.

Escape hatch. theme.tokens sets any --unn-* token by name; the names follow the design system and may change between minor versions, so prefer brand. theme.shadowTokens (a :host { … } block) still works. Later wins: defaults → shadowTokens → brand → tokens. Names that are not --unn-*, unknown brand roles, and values containing ;, braces, angle brackets, backslashes or newlines are ignored. theme.primaryColor and theme.borderRadius are deprecated aliases of brand.primary and brand.radius.

The defaults are the design system's :root tokens (ui/designsystem/src/styles/globals.css), copied into src/ui/theme/defaultTokens.generated.ts by pnpm tokens; a test fails when the two drift or when the widget uses a token without a default.

UnnoodleConfig Interface

| Property | Type | Required | Default | Description | | -------------------- | ------------------------------- | -------- | -------------------------- | --------------------------------------------------------------- | | apiKey | string | ✅ | — | Workspace API key (pk_live_…). Get it from Settings → API Key | | context | string | ✅ | — | Context slug to load (auto-created if new) | | environment | "dev" \| "staging" \| "prod" | | "prod" | Target environment | | baseUrl | string | | https://api.unnoodle.com | API base URL override | | getAuthToken | () => string \| null | | — | Returns current user's auth token | | theme.brand | UnnoodleBrand | | design system look | Your brand: primary, secondary, hover, font, radius… (Theming) | | theme.tokens | Record<string, string> | | — | Any --unn-* token by name; escape hatch (Theming) | | theme.shadowTokens | string | | — | A :host { … } block of --unn-* tokens | | customMetadata | () => Record<string, unknown> | | — | Additional context metadata | | _plugins | UnnoodlePlugin[] | | — | Internal plugin hooks |


Security

  • Auth tokens are resolved at request time and never stored
  • All API communication uses HTTPS
  • No secrets are bundled in the SDK
  • Shadow DOM prevents CSS bleed-in and bleed-out
  • The SDK fails silently on misconfiguration — it will never crash the host app

Product Memory — Developer API (V2)

The V2 Product Memory sub-system ships as part of the package and is exported for advanced SDK consumers. These modules work in any browser environment and have no React/framework dependency.

StateWatcher

Attaches a MutationObserver to any DOM element and fires a typed StateDiff callback when meaningful changes are detected.

import { StateWatcher, type StateDiff } from "@unnoodle/browser";

const watcher = new StateWatcher();
const btn = document.getElementById("submit-btn")!;

watcher.watch(btn, (diff: StateDiff) => {
  console.log(diff.type); // "text" | "attribute" | "structure"
  console.log(diff.before); // value before the change
  console.log(diff.after); // value after the change
  console.log(diff.attribute); // attribute name (only when type === "attribute")
});

// Clean up
watcher.unwatch();

Behaviour:

  • Changes are debounced for 500 ms — rapid mutations produce a single callback.
  • style attribute mutations are filtered out (noise filter).
  • Text changes (textContent assignments) are classified as type: "text".
  • Attribute changes (class, disabled, aria-*) are classified as type: "attribute".
  • Child element additions/removals are classified as type: "structure".
  • unwatch() cancels any pending callback and disconnects the observer.

inferMeaning(diff, el)

Rule-based semantic label inference from a StateDiff.

import { StateWatcher, inferMeaning } from "@unnoodle/browser";

const watcher = new StateWatcher();

watcher.watch(document.getElementById("submit-btn")!, (diff) => {
  const meaning = inferMeaning(diff, document.getElementById("submit-btn")!);
  // "Loading state" | "Error state" | "Disabled state" | … | null
  if (meaning) {
    console.log("Detected state:", meaning);
  }
});

Inferred labels:

| Trigger | Label | | --------------------------------------------------- | ----------------------------- | | Text contains "loading" / "submitting" / "saving" | "Loading state" | | Text contains "error" / "failed" / "invalid" | "Error state" | | Text contains "success" / "saved" / "done" | "Success state" | | Class "loading" / "spinner" / "pending" added | "Loading state" | | Class "disabled" / "inactive" added | "Disabled state" | | Class "error" / "invalid" / "danger" added | "Error state" | | Class "active" / "selected" / "open" added | "Active state" | | Class "collapsed" / "hidden" added | "Collapsed state" | | disabled attribute added | "Disabled state" | | aria-expanded → "true" | "Expanded state" | | aria-expanded → "false" | "Collapsed state" | | aria-busy → "true" | "Loading state" | | aria-label changed | "Label changed to: <value>" | | Structure change or unknown | null |

LiveRenderer

Highlights a live DOM element with a fixed-position ring (useful for showing which element a stored ProductMemory state belongs to).

import { LiveRenderer } from "@unnoodle/browser";

const renderer = new LiveRenderer();

// Highlight an element
renderer.show(document.getElementById("submit-btn")!);
// → scrolls element into view + draws violet highlight ring

// Remove highlight
renderer.hide();

Calling hide() before show() is safe. Calling show() twice replaces the previous ring.


Changelog

See CHANGELOG.md for version history.


Release

Releases are published by GitHub Actions (.github/workflows/publish-browser.yml), never from a laptop. Pushing a tag browser-v<version> builds, tests and publishes that version to npm, signed in with npm trusted publishing (no npm token is stored anywhere).

cd ui/browser
npm run release:patch          # or release:minor / release:major — bumps package.json only
VERSION=$(node -p "require('./package.json').version")
git commit -am "chore(browser): release $VERSION"
git tag "browser-v$VERSION"
git push origin main "browser-v$VERSION"

The workflow refuses to publish if the tag does not match package.json, if the version is already on npm, if the tests fail, or if the packed tarball does not install and import in an empty project.

To check a release without publishing: GitHub → Actions → "Publish @unnoodle/browser" → Run workflow (a dry run: everything but the publish).

One-time setup (already done if a release has gone out): on npmjs.com, @unnoodle/browser → Settings → Trusted publishing → GitHub Actions, with organization unnoodle, repository unnoodle, workflow publish-browser.yml.


License

MIT © Unnoodle