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

paperfalcon

v1.0.1

Published

Paperfalcon embeddable widget SDK — render dynamic, template-driven widgets across any web environment

Downloads

330

Readme

Paperfalcon Widget SDK

Package: paperfalcon · Version: 1.0.0

Embeddable, template-driven widget platform for any web page. Define widgets via API (Liquid template + data + triggers), then render them through an npm import or a single <script> tag.


Table of contents

  1. Overview
  2. Installation & build
  3. Architecture
  4. Quick start
  5. Development guide
  6. Production guide
  7. Core API — WidgetSDK
  8. API contract
  9. Widget configuration
  10. Widget types
  11. Templates (Liquid)
  12. Triggers
  13. Analytics & frequency
  14. Auth
  15. Plugins
  16. Script-tag embed
  17. Events
  18. Dev tools
  19. Examples
  20. Troubleshooting

Overview

| Capability | Details | | ------------ | ------------------------------------------------------------------------------------------------------ | | Delivery | ESM / CJS (npm) + UMD/IIFE (<script> tag) | | Templates | LiquidJS — content comes from your API | | Widget types | 12: static, list, form, interactive, carousel, timer, media, social, data, container, chatbot, generic | | Triggers | Time, scroll, exit intent, page load, location, behavior, custom | | Analytics | Impressions, sessions, frequency capping | | Auth | Bearer token or API key on widget config fetches | | Plugins | Analytics, Cache, Security, Performance (+ custom) | | Dev tools | MockServer, DevPanel, TemplatePreview, TriggerSimulator, … |

Design principles

  • Template-first — new widget layouts do not require SDK code changes; update the API response.
  • Dual delivery — one codebase builds npm packages and a browser bundle.
  • Host-page friendly — mount into any DOM element by id.

Installation & build

Install (consumers)

npm install paperfalcon
# or
pnpm add paperfalcon

Develop this repo

pnpm install
pnpm run build           # ESM + CJS + UMD + types → dist/ (dev, with sourcemaps)
pnpm run build:publish   # Minified, no sourcemaps — what ships to npm
pnpm run typecheck       # tsc --noEmit
pnpm run clean           # remove dist/
pnpm run pack:check      # publish build + npm pack --dry-run

Full npm release steps: see documents/sdk/npm-publish.md.

Build outputs

| Path | Format | Use | | ---------------------------- | ------------- | ----------------------------- | | dist/esm/index.js | ESM | Bundlers (import) | | dist/cjs/index.js | CommonJS | require() | | dist/umd/widget-sdk.js | IIFE | Script tag (readable / debug) | | dist/umd/widget-sdk.min.js | IIFE minified | Script tag (production) | | dist/types/ | .d.ts | TypeScript |

Package exports:

  • paperfalcon → main SDK
  • paperfalcon/embed → script-tag / embed entry

Local preview (after build):

npx serve .
# open http://localhost:3000 (or the URL printed)

Architecture

Host page
   │
   ├─ npm:  import { WidgetSDK } from 'paperfalcon'
   └─ script:  <script src="…/widget-sdk.min.js">
                    │
                    ▼
              WidgetSDK.init(config)
                    │
         ┌──────────┼──────────┐
         ▼          ▼          ▼
   ConfigManager  Auth     PluginManager
   GET /widgets/:id
         │
         ▼
   WidgetManager → registry (static, form, …)
         │
         ▼
   BaseWidget.mount()
     → Liquid template + data
     → inject HTML into container
         │
   TriggerManager (optional show/hide rules)
   Tracker (impressions / frequency)

Request flow for sdk.load(containerId, widgetId)

  1. Resolve DOM #containerId
  2. GET {apiUrl}/widgets/{widgetId} (with auth headers)
  3. Validate config (meta, template, …)
  4. Register triggers / check frequency cap
  5. Instantiate widget class by meta.type
  6. Render Liquid → sanitize HTML → mount into container
  7. Track impression (if analytics enabled)

Quick start

npm / bundler

<div id="promo"></div>
import { WidgetSDK } from 'paperfalcon';

const sdk = new WidgetSDK();
sdk.init({
  apiUrl: 'https://api.example.com',
  token: 'YOUR_TOKEN',
  debug: false,
});

await sdk.load('promo', 'widget-abc123');

Script tag

<div data-widget-id="widget-abc123"></div>

<script
  src="https://cdn.example.com/widget-sdk.min.js"
  data-api-url="https://api.example.com"
  data-token="YOUR_TOKEN"
  data-auto-init="true"
></script>

Manual (no auto-init):

<div id="promo"></div>
<script src="./dist/umd/widget-sdk.min.js"></script>
<script>
  const sdk = new WidgetSDK.WidgetSDK();
  sdk.init({ apiUrl: 'https://api.example.com', token: 'YOUR_TOKEN' });
  sdk.load('promo', 'widget-abc123');
</script>

Development guide

Use this when building widgets locally without a backend.

1. Build the SDK

pnpm install
pnpm run build

2. Mock the API with MockServer

MockServer patches fetch and answers GET …/widgets/:id. Start it before sdk.load().

<!DOCTYPE html>
<html>
<body>
  <div id="root"></div>
  <script src="./dist/umd/widget-sdk.min.js"></script>
  <script>
    const mock = new WidgetSDK.MockServer({
      delay: 100,
      widgets: [
        {
          meta: { id: 'hello', name: 'Hello', type: 'static', version: '1' },
          template: '<div class="banner"><h2>{{ data.title }}</h2><p>{{ data.body }}</p></div>',
          data: { title: 'Hello', body: 'Local mock works.' },
        },
      ],
    });
    mock.start();

    const sdk = new WidgetSDK.WidgetSDK();
    sdk.init({ apiUrl: 'http://localhost:3000', debug: true });
    sdk.load('root', 'hello');
  </script>
</body>
</html>

Serve with a real HTTP server (not file://):

npx serve .
# or: python3 -m http.server 8080

3. Debug mode

sdk.init({ apiUrl: '…', debug: true });
  • Verbose logging
  • Floating DevPanel (events / widgets)
  • sdk.getDevPanel()
  • window.__widgetSDK exposed by DevPanel

4. Seed config without HTTP

sdk.setWidgetConfig('hello', {
  meta: { id: 'hello', name: 'Hello', type: 'static', version: '1' },
  template: '<p>{{ data.title }}</p>',
  data: { title: 'Cached config' },
});
// Still call load — ConfigManager uses cache when TTL allows
await sdk.load('root', 'hello');

5. Watch rebuild (source changes)

pnpm run build:watch   # if enabled via build.mjs --watch
# or rebuild after edits:
pnpm run build

Hard-refresh the browser after rebuilding dist/umd/.

6. Recommended local checklist

  • [ ] pnpm run build succeeds
  • [ ] Page served over http://localhost…
  • [ ] MockServer.start() before any load()
  • [ ] Container element exists (id matches load first arg)
  • [ ] Widget meta.type is a registered type
  • [ ] Templates use {{ data.fieldName }}
  • [ ] Triggers set "enabled": true when used
  • [ ] Prefer sdk.destroy(widgetId) over instance.destroy() alone

Production guide

Choose a delivery mode

| Mode | When to use | Bundle | | -------------- | ----------------------------------------------------- | ------------------------------------ | | npm | App already has a bundler (React, Vue, Vite, Next, …) | ESM/CJS from package | | Script tag | Marketing sites, CMS, no build step | Host widget-sdk.min.js on your CDN |

npm production

import { WidgetSDK } from 'paperfalcon';

const sdk = new WidgetSDK();
sdk.init({
  apiUrl: 'https://api.yourproduct.com',
  token: process.env.WIDGET_SDK_TOKEN, // inject safely; never hardcode in public repos
  debug: false,
  timeout: 10000,
  cacheTtl: 60_000,
  analytics: true,
  headers: {
    'X-Client': 'web',
  },
});

await sdk.load('slot-hero', 'promo-summer');

Script-tag production

  1. Deploy dist/umd/widget-sdk.min.js to your CDN (versioned URL recommended).
  2. Ensure CORS + HTTPS on your widget API.
  3. Embed:
<div id="wgt-hero" data-widget-id="promo-summer"></div>
<script
  src="https://cdn.yourproduct.com/widget-sdk/1.0.0/widget-sdk.min.js"
  data-api-url="https://api.yourproduct.com"
  data-token="PUBLIC_OR_SCOPED_TOKEN"
  data-auto-init="true"
  defer
></script>

Prefer short-lived / scoped tokens for public pages. Treat anything in HTML as public.

Backend requirements

Your API must implement:

GET {apiUrl}/widgets/{widgetId}
Authorization: Bearer <token>   # when using bearer tokens

Response shape:

{
  "success": true,
  "data": { /* WidgetConfig — see below */ }
}

Error statuses the client maps specially: 401, 404, 429, plus generic non-OK.

Production config recommendations

| Option | Recommendation | | ----------- | ---------------------------------------------------- | | debug | false | | Bundle | widget-sdk.min.js | | timeout | 8–15s | | cacheTtl | 30–120s (or 0 to disable) | | analytics | true + set tracker endpoint if you ingest events | | HTTPS | Required for API + script | | CSP | Allow your CDN script + API origin |

Lifecycle in SPAs

// On route enter
await sdk.load('mount', widgetId);

// On route leave / unmount
sdk.destroy(widgetId);

// Full teardown (logout, app exit)
sdk.destroyAll();

After destroyAll(), call init() again before loading widgets.

Security notes

  • HTML from templates is sanitized (strips <script>, event handlers like onclick, javascript: URLs).
  • Put interactive behavior in SDK bindings (data-action, widget events), not inline JS in templates.
  • Use SecurityPlugin to restrict allowed origins when embedding on partner sites.

Core API — WidgetSDK

import { WidgetSDK, SDK_VERSION } from 'paperfalcon';

const sdk = new WidgetSDK();

init(config: SDKConfig): void

Must be called once before load. Merges with defaults:

| Field | Type | Default | Description | | ------------------- | ----------------------- | -------------------- | -------------------------------- | | apiUrl | string | required | Base URL for widget API | | token | string | — | Bearer token | | locale | string | 'en' | Locale hint | | debug | boolean | false | Logs + DevPanel | | timeout | number | 10000 | HTTP timeout (ms) | | cacheTtl | number | 60000 | Config cache TTL (ms); 0 = off | | analytics | boolean | true | Enable Tracker | | headers | Record<string,string> | — | Extra HTTP headers | | fetch | typeof fetch | — | Custom fetch (tests / mocks) | | plugins | Plugin[] | — | Installed at init | | defaults | Partial<WidgetConfig> | — | Merged into each widget config | | containerSelector | string | '[data-widget-id]' | Embed discovery |

Calling init twice without destroyAll() logs a warning and returns.

load(containerId, widgetId?): Promise<WidgetInstance>

  • containerId — DOM element id
  • widgetId — API id; if omitted, uses data-widget-id on the element, then the element id

Returns a WidgetInstance:

interface WidgetInstance {
  id: string;
  config: WidgetConfig;
  state: WidgetState;
  container: HTMLElement;
  mount(): Promise<void>;
  unmount(): void;
  update(data: Partial<WidgetData>): Promise<void>;
  destroy(): void;
  getState(): WidgetState;
  on(event: string, handler: (data: unknown) => void): void;
  off(event: string, handler: (data: unknown) => void): void;
}

Other methods

| Method | Description | | ----------------------------------- | ------------------------------------------------------- | | destroy(widgetId) | Unmount + remove from registry (preferred) | | destroyAll() | Destroy widgets, stop triggers/tracker, allow re-init | | use(plugin) | Register a plugin after init | | on / emit | SDK event bus | | getWidget(id) / getAllWidgets() | Access instances | | setToken(token) | Update bearer token | | setWidgetConfig(id, config) | Seed / override cache | | invalidateCache(id?) | Clear cached configs | | getDevPanel() / getTracker() | Debug / analytics accessors | | version | '1.0.0' |


API contract

GET /widgets/:id

Success

{
  "success": true,
  "data": {
    "meta": {
      "id": "promo-summer",
      "name": "Summer Promo",
      "type": "static",
      "version": "1",
      "description": "optional",
      "tags": ["promo"]
    },
    "template": "<div>… Liquid …</div>",
    "data": { },
    "styles": { },
    "settings": { },
    "triggers": [ ],
    "frequency": { },
    "analytics": { }
  }
}

Required fields: meta.id, meta.type, meta.name, meta.version, template (string; may be empty for some types like chatbot which ignore it).


Widget configuration

meta

| Field | Required | Notes | | --------- | -------- | --------------------- | | id | yes | Stable widget id | | name | yes | Display name | | type | yes | One of the 12 types | | version | yes | Config version string |

data

Arbitrary JSON used in Liquid as {{ data.* }}. Shape depends on widget type (see below).

styles (optional)

{
  position?: { fixed?: boolean; top?: string; bottom?: string; left?: string; right?: string; zIndex?: number };
  dimensions?: { width?: string; height?: string; maxWidth?: string; maxHeight?: string };
  className?: string;
  inlineStyles?: Record<string, string>;
  /** Raw CSS string from the API — injected automatically */
  css?: string;
  /** Remote stylesheet URL(s) */
  cssUrl?: string | string[];
}
  • position / dimensions become host rules on #containerId (good for full-screen popups).
  • css / cssUrl are injected into the widget Shadow Root (style isolation is automatic).
  • Prefer API css over inline style="" in templates.

Shadow DOM (automatic)

Shadow DOM is always enabled by default. API configs and integrators do not set shadowDom / shadowMode.

What the SDK does for you:

  1. Attaches a Shadow Root on the container
  2. Mounts template HTML inside the shadow tree
  3. Injects API css / cssUrl inside the shadow (isolated from the host page)
  4. Applies host position / dimensions on the light-DOM container
{
  "styles": { "css": ".card { padding: 1rem; background: #fff; }" }
}

Opt out only if needed: "settings": { "shadowDom": false }.

Shadow events are retargeted on the host — use event.composedPath() for click handling from outside.

settings (optional)

| Field | Default | Notes | |-------|---------|-------| | closable | true | Flag for hosts / templates | | overlay | false | Flag — implement overlay in template | | animation | 'fade' | 'fade' \| 'slide' \| 'scale' \| 'none' | | animationDuration | 300 | ms | | autoClose | 0 | ms; 0 = off | | resetOnClose | false | |

Overlay / close UI is typically implemented in the Liquid template; settings are metadata for your product.

frequency (optional)

{
  maxShows?: number;          // 0 / omitted = unlimited
  unit?: 'session' | 'day' | 'week' | 'month' | 'forever';
  minInterval?: number;       // seconds between shows
  resetOnConversion?: boolean;
}

When capped, load() returns a no-op instance and skips mount.


Widget types

static

Banners, announcements, popups.

Data: title?, content?, imageUrl?, ctaText?, ctaUrl?

list

Data: items[] (id, title?, subtitle?, imageUrl?, url?, badge?), layout?: 'list'|'grid', columns?, title?

Event: item:click{ itemId }

form

Data: fields[], submitLabel?, successMessage?, errorMessage?, action?, method?

Field: name, type (text|email|tel|number|textarea|select|checkbox|radio|hidden), label?, placeholder?, required?, options?, defaultValue?

Event: form:submit{ values, widgetId } (on the instance)

interactive

Custom click actions. Requires your own template.

Data: content, actions?: Record<string, InteractiveAction>, initialState?

DOM hooks: [data-action="…"], [data-toggle="selector"]

Action types: show | hide | toggle | navigate | emit

Event: interaction

carousel

Data: slides[], autoPlay?, autoPlayInterval? (default 4000), showArrows?, showIndicators?, loop?

Event: slide:change{ index }

timer

Countdown to targetDate (ISO string or ms).

Template extras: remaining.days|hours|minutes|seconds|total, expired, id

Event: timer:expired

media

Data: mediaType: 'video'|'audio'|'image'|'youtube'|'vimeo', src, posterUrl?, autoPlay?, muted?, loop?, controls?, caption?, width?, height?

Events: media:play, media:pause, media:ended

social

Data: platform, embedCode?, shareUrl?, shareText?, username?, postId?, showFollowButton?

Event: social:share

data

Fetches remote JSON and re-renders.

Data: source: { type, url?, method?, headers?, body?, refreshInterval?, path? }

Method: refresh(): Promise<void> (on the widget instance when cast/used as DataWidget)

container

Layout shell with named slots for child widgets.

Data:

{
  layout?: 'flex' | 'grid' | 'stack';
  columns?: number;
  gap?: string;
  slots: { id: string; widgetId?: string; selector?: string }[];
}

Method: getSlot(slotId): HTMLElement | null

Typical pattern: load container, then load children into slot element ids (slot-{id}).

chatbot

Built-in chat UI (ignores Liquid template).

Data: botName?, placeholder?, welcomeMessage?, apiUrl?, websocketUrl?, maxMessages?

Transport order: WebSocket → REST POST { message, history } → local echo.

generic

Fallback type. Renders your template, or dumps {{ data | json }}.


Templates (Liquid)

Widget data is available as data.* (and also as top-level keys for convenience):

<div class="promo">
  <h2>{{ data.title }}</h2>
  {% if data.imageUrl %}
    <img src="{{ data.imageUrl }}" alt="{{ data.title }}" />
  {% endif %}
  {% for item in data.items %}
    <p>{{ item.name }} — {{ item.price | currency: '$' }}</p>
  {% endfor %}
</div>

Built-in filters: truncate_words, currency, json, default (+ standard Liquid filters).

Rendered HTML is sanitized before insert (no <script>, no on* handlers).


Triggers

Attached on the widget config. Evaluated by TriggerManager on a ~500ms poll (+ exit-intent listener).

{
  "triggers": [
    {
      "id": "show-after-3s",
      "enabled": true,
      "conditionOperator": "AND",
      "once": true,
      "conditions": [
        { "type": "time", "params": { "delay": 3 } }
      ],
      "actions": [
        { "type": "show", "params": { "containerId": "popup-root" } }
      ]
    }
  ]
}

enabled must be true. If omitted, the trigger is ignored.

Conditions

| type | Params | Meaning | | ------------ | --------------------------- | ------------------------------- | | time | delay (seconds) | Session duration ≥ delay | | scroll | threshold (%, default 50) | Scroll depth ≥ threshold | | pageLoad | — | Always true | | exitIntent | — | Cursor leaves toward top | | location | pathname | Path contains string | | behavior | pageViews | Session page views ≥ N | | custom | key, value | context.custom[key] === value |

Actions

| type | Params | Effect | | --------------- | ------------------ | --------------------------- | | show | containerId | Show element | | hide | containerId | Hide element | | updateContent | widgetId, data | widget.update(data) | | delay | ms | Wait before next action | | custom | name, … | Runs registerCustomAction |

Show/hide toggle visibility of a DOM node; they do not by themselves call sdk.load. Typical popup flow: load (or pre-create) the widget, keep container hidden, then show when conditions match — or call sdk.load from your own code on a timer/button.


Analytics & frequency

When analytics: true (default), init creates a Tracker:

  • Impressions recorded on successful load
  • Session id in storage (30 min inactivity → new session)
  • Frequency checks before mount when frequency is set
const tracker = sdk.getTracker();
tracker?.events.track('conversion', 'promo-summer', { plan: 'pro' });

Event types: impression | click | submit | close | conversion | error | custom

To flush to your backend, configure the tracker’s analytics endpoint (batch upload). Without an endpoint, events stay local / no-op on flush.


Auth

sdk.init({ apiUrl: '…', token: 'eyJ…' });
sdk.setToken('new-token'); // later
  • Bearer → Authorization: Bearer …
  • Utilities also exist for API key (X-Api-Key) via AuthManager if you wire them yourself
  • TokenManager / AuthInterceptor are available as building blocks for custom auth flows

Config fetch refreshes the token when marked expired (if refresh is configured on AuthManager).


Plugins

import {
  WidgetSDK,
  AnalyticsPlugin,
  CachePlugin,
  SecurityPlugin,
  PerformancePlugin,
} from 'paperfalcon';

const sdk = new WidgetSDK();
sdk.init({
  apiUrl: 'https://api.example.com',
  plugins: [
    new AnalyticsPlugin(),
    new CachePlugin(5 * 60 * 1000),
    new SecurityPlugin({ allowedOrigins: ['example.com'] }),
    new PerformancePlugin(),
  ],
});

// or later
sdk.use(new PerformancePlugin());

Plugin interface

interface Plugin {
  name: string;
  version?: string;
  install(sdk: SDKInstance): void;
  hooks?: Partial<Record<PluginHook, PluginHookFn>>;
}

Hooks: beforeInit, afterInit, beforeLoad, afterLoad, beforeRender, afterRender, beforeDestroy, afterDestroy, onError, onEvent

The SDK currently invokes beforeLoad / afterLoad during load().

| Plugin | Role | | ------------------- | --------------------------------------------------------- | | AnalyticsPlugin | Listens to mount/click/submit-style bus events | | CachePlugin | Extra in-memory / sessionStorage helpers | | SecurityPlugin | Origin allowlist + sanitize hooks | | PerformancePlugin | Render timing (getTimeline(), getAverageRenderTime()) |


Script-tag embed

Attributes on the SDK <script>

| Attribute | Purpose | | ---------------- | ------------------------------------------------- | | data-api-url | API base URL | | data-token | Auth token | | data-widget-id | Optional: auto-create a host and load this widget | | data-debug | "true" enables debug | | data-auto-init | Default on; "false" disables auto discovery |

Placeholders

<div data-widget-id="promo-summer"></div>

On DOM ready, embed runtime:

  1. Parses script attributes
  2. inits an SDK instance → window.widgetSDKInstance
  3. Discovers [data-widget-id] and calls load
  4. Optionally loads data-widget-id from the script into a generated container

UMD global: WidgetSDK (class at WidgetSDK.WidgetSDK, plus MockServer, etc.).


Events

SDK-level (sdk.on)

Emitted today:

| Event | Payload (approx.) | | ----------------- | -------------------- | | sdk:init | { version } | | sdk:destroy | {} | | widget:load | { widgetId, type } | | widget:mount | { widgetId } | | widget:update | { widgetId, data } | | widget:destroy | { widgetId } | | plugin:register | { name } | | auth:token:set | {} |

sdk.on('widget:mount', (data) => console.log(data));

Widget-level (instance.on)

Depends on type: item:click, form:submit, interaction, slide:change, timer:expired, media:*, social:share, …

const w = await sdk.load('form-root', 'lead-form');
w.on('form:submit', (payload) => {
  console.log(payload);
});

Dev tools

| Tool | Use | | -------------------- | -------------------------------------- | | MockServer | Fake GET /widgets/:id in the browser | | DevPanel | Floating panel when debug: true | | DevTools | inspectAll(), exposeOnWindow() | | EventViewer | Capture / list bus events | | TriggerSimulator | Fire triggers with context overrides | | TemplatePreview | preview(template, data), validate | | StateManager | Snapshot / diff widget state | | PerformanceMonitor | Marks / measures | | WidgetInspector | Visual outline overlay | | ConfigEditor | Live JSON editing helper |

import { MockServer, TemplatePreview } from 'paperfalcon';

const preview = new TemplatePreview();
const html = await preview.preview('<h1>{{ data.title }}</h1>', { title: 'Hi' });

Examples

Popup (static + overlay template)

await sdk.load('popup-root', 'promo-popup');
// Widget config styles: position.fixed + full width/height
// Template: fixed overlay + centered card + [data-close] button
// Close: sdk.destroy('promo-popup')

See index.html in this repo for a full MockServer popup demo.

Container + children

await sdk.load('layout-root', 'container-home');
// slots render as #slot-header, #slot-sidebar, …
await sdk.load('slot-header', 'banner-top');
await sdk.load('slot-main', 'product-list');

Form submit handling

const form = await sdk.load('lead', 'lead-capture');
form.on('form:submit', (data) => {
  // { values, widgetId }
  fetch('/api/leads', { method: 'POST', body: JSON.stringify(data.values) });
});

Time-delayed show (host-driven)

// Keep #popup-root empty until ready
setTimeout(() => sdk.load('popup-root', 'promo-popup'), 3000);

Or use a trigger show action on a pre-mounted, hidden container.


Troubleshooting

| Symptom | Cause / fix | | ---------------------------- | -------------------------------------------------------------------------- | | Blank {{ data.* }} text | Rebuild after template-context fix; confirm Liquid uses data. prefix | | Button load does nothing | Call sdk.destroy(id) before reloading the same widget id | | CONTAINER_NOT_FOUND | Element id missing or script ran before DOM | | Network / CORS errors | Serve over HTTP; API must allow your origin | | Mock ignored | mock.start() before sdk.init / load; URL must contain /widgets/:id | | Trigger never fires | Set "enabled": true | | Popup has no dimmed backdrop | Put overlay markup in the template, not only settings.overlay | | file:// broken | Use npx serve . | | Stale UI after code change | pnpm run build + hard refresh |

Destroy correctly

// ✅ removes from registry — safe to load again
sdk.destroy('promo-popup');

// ⚠️ instance-only destroy leaves a stale registry entry
instance.destroy();

Project layout

src/
  WidgetSDK.ts          Main class
  index.ts              npm exports
  embed.ts              Script-tag entry
  core/                 Config, registry, manager, template, plugins, auth
  widgets/              12 widget implementations
  triggers/             Conditions + actions + scheduler
  analytics/            Tracker, session, frequency
  embed/                Script discovery / host helpers
  plugins/              Built-in plugins
  auth/                 Token helpers
  dev/                  MockServer, DevPanel, …
  types/                TypeScript types
  utils/                HTTP, sanitize, DOM, …
build.mjs               esbuild multi-format build
dist/                   Build output (generated)
index.html              Local popup demo
README.md               This document

License

MIT