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

blocfeed

v0.8.1

Published

Drop-in feedback widget for React — element picking, screenshots, and video recording.

Readme

BlocFeed (blocfeed)

Drop-in in-app feedback widget for Next.js and React:

  • Safe "feedback mode" with DOM element picking (blocks host app clicks while active)
  • Optional element + full page screenshots (capture code is lazy-loaded)
  • Typed, JSON-serializable FeedbackPayload with contextual metadata
  • Submits directly to the BlocFeed platform via blocfeed_id
  • Optional selection.componentName + selection.testId for faster triage
  • First-class user identity — attach user.id, email, name to every submission
  • Offline queue — failed submissions are stored and retried automatically
  • Customizable position, theme, and retry behavior
  • Accessible — keyboard navigation, focus trapping, ARIA attributes
  • Feedback categories — pill selector (Bug, Feature, UX, General) included in payload
  • Dark / Light mode"dark", "light", or "auto" (follows system preference)
  • Conditional display — show/hide widget by route pattern or custom predicate
  • Programmatic APIref.open(), ref.close(), ref.submit(msg) via React ref
  • Video recording — record a short screen capture clip (via getDisplayMedia) to show bug reproduction steps
  • Secret leak detection — scans client-side code for exposed API keys, database credentials, and tokens

Docs live in docs/ (start at docs/index.md). Architecture pointers are in ARCHITECTURE.md.

Install

npm install blocfeed

Peer deps: react, react-dom.

Get your blocfeed_id

Create a project in the BlocFeed dashboard (https://blocfeed.com). Each project has a unique blocfeed_id that the SDK uses to link incoming feedback to the correct project.

Submission endpoint

Submissions are sent to the BlocFeed platform ingestion API at https://blocfeed.com/api/feedback. Custom/external endpoints are intentionally not supported.

Next.js (App Router) — copy/paste

app/layout.tsx

import type { ReactNode } from "react";
import { BlocFeedWidget } from "blocfeed";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <BlocFeedWidget blocfeed_id="bf_your_project_blocfeed_id" />
      </body>
    </html>
  );
}

Next.js (Pages Router)

Use a client-only dynamic import:

import dynamic from "next/dynamic";

const BlocFeedWidget = dynamic(
  () => import("blocfeed").then((m) => m.BlocFeedWidget),
  { ssr: false }
);

export default function App({ Component, pageProps }) {
  return (
    <>
      <Component {...pageProps} />
      <BlocFeedWidget blocfeed_id="bf_your_project_blocfeed_id" />
    </>
  );
}

React (SPA)

import { BlocFeedWidget } from "blocfeed";

export function App() {
  return (
    <>
      {/* your app */}
      <BlocFeedWidget blocfeed_id="bf_your_project_blocfeed_id" />
    </>
  );
}

Configuration

All configuration is passed via the config prop on <BlocFeedWidget> or <BlocFeedProvider>:

<BlocFeedWidget
  blocfeed_id="bf_..."
  config={{
    // User identity (attached to every submission)
    user: {
      id: "user_123",
      email: "[email protected]",
      name: "Jane Doe",
    },

    // Widget position, theme & trigger style
    ui: {
      position: "bottom-right", // "bottom-left" | "top-right" | "top-left"
      triggerStyle: "dot",      // "classic" | "bubble" | "edge-tab" | "pulse-ring" | "minimal" | "icon-pop" | "beacon" | "typewriter"
      triggerLabel: "Feedback",  // custom label text
      shortcut: "mod+shift+f",  // keyboard shortcut to toggle widget
      zIndex: 99999,
      theme: {
        accentColor: "#12D393",
        panelBackground: "rgba(0, 0, 0, 0.95)",
        panelForeground: "#ffffff",
        fontFamily: "Inter, sans-serif",
        mode: "dark",           // "dark" | "light" | "auto"
      },
      categories: ["bug", "feature", "ux", "general"],  // feedback category pills
      showOn: ["/dashboard/*", "/app/*"],                // route-based visibility
    },

    // Diagnostics (console + network capture, including XHR)
    diagnostics: {
      console: true,
      network: true,             // captures both fetch and XMLHttpRequest
      // ignoreUrls: [],          // override default blocked domains (see docs below)
    },

    // Video recording (screen capture)
    recording: {
      enabled: true,
      maxDurationMs: 30_000,
      videoBitsPerSecond: 2_500_000,
    },

    // Screenshot defaults
    capture: {
      element: true,
      fullPage: false,
      mime: "image/png",     // or "image/jpeg"
      quality: 0.92,         // JPEG quality (0–1)
      timeoutMs: 12000,
      maxDimension: 2048,
      pixelRatio: 2,
    },

    // Retry & transport
    transport: {
      timeoutMs: 12000,      // per-request timeout
      maxAttempts: 2,         // retry count
      backoffMs: 500,         // base backoff delay
    },

    // Security — secret leak detection
    security: {
      secretScan: true,
      scanTargets: ["hydration", "scripts", "meta", "dom"],
    },

    // Metadata enrichment
    metadata: {
      enabled: true,
      enrich: async (context) => ({
        orgId: "org_abc",
        plan: "pro",
      }),
    },

    // Picker rules
    picker: {
      ignoreSelectors: ["[data-private]"],
      isSelectable: (el) => el.tagName !== "HTML",
    },
  }}
/>

User Identity

Attach user information to every feedback submission:

<BlocFeedWidget
  blocfeed_id="bf_..."
  config={{
    user: {
      id: currentUser.id,
      email: currentUser.email,
      name: currentUser.name,
    },
  }}
/>

The user object is included in the payload as a top-level field and its values are also merged into metadata as userId, userEmail, userName.

Widget Position

Place the trigger button in any corner:

config={{
  ui: { position: "bottom-left" }  // default: "bottom-right"
}}

Options: "bottom-right" | "bottom-left" | "top-right" | "top-left"

Trigger Styles

Customize the trigger button's appearance and animation:

config={{
  ui: { triggerStyle: "dot" }
}}

| Style | Description | Requires framer-motion | |-------|-------------|----------------------| | "classic" | Default pill button with colored dot (no animation) | No | | "dot" | Breathing dot that expands to pill on hover | Yes | | "bubble" | Floating chat-bubble icon with tooltip on hover | Yes | | "edge-tab" | Thin tab anchored to screen edge, slides out on hover | Yes | | "pulse-ring" | Sonar-style pulsing rings around a dot, expands on hover | Yes | | "minimal" | Text-only "Feedback" with animated underline on hover | Yes | | "icon-pop" | Wobbling icon that pops and reveals text on hover | Yes | | "beacon" | Lighthouse-style sweeping beam with glowing dot, expands on hover | Yes | | "typewriter" | Text types out character by character with blinking cursor | Yes |

All animated styles include smooth enter/exit transitions. Animations automatically simplify when prefers-reduced-motion: reduce is active.

Custom trigger label

config={{
  ui: { triggerLabel: "Report Bug" }
}}

The label defaults to "Feedback" and is used across all trigger styles.

Keyboard shortcut

Open/close the widget with a keyboard shortcut:

config={{
  ui: { shortcut: "mod+shift+f" }
}}

Use mod for platform-aware behavior (Ctrl on Windows/Linux, Cmd on Mac). Supports ctrl, meta/cmd, shift, alt/option modifiers.

Installing framer-motion

All styles except "classic" require framer-motion as a peer dependency:

npm install framer-motion

i18n / Localization

Override all UI text via config.ui.strings:

config={{
  ui: {
    strings: {
      triggerLabel: "Rückmeldung",
      panelTitle: "Rückmeldung",
      hintText: "Klicken Sie auf ein Element. Drücken Sie Esc zum Abbrechen.",
      cancelButton: "Abbrechen",
      rePickButton: "Neu wählen",
      closeButton: "Schließen",
      textareaPlaceholder: "Was ist passiert? Was haben Sie erwartet?",
      screenshotElement: "Element-Screenshot",
      screenshotFullPage: "Ganze Seite",
      capturingText: "Screenshots werden aufgenommen…",
      submittingText: "Wird gesendet…",
      successText: "Gesendet. Vielen Dank!",
      toastText: "Feedback gesendet",
      sendButton: "Senden",
    },
  },
}}

All fields are optional — only override the ones you need. Category labels are also localizable:

strings: {
  categoryBug: "Fehler",
  categoryFeature: "Funktion",
  categoryUx: "UX",
  categoryGeneral: "Allgemein",
}

Feedback Categories

Show a pill selector in the feedback form so users can tag their feedback:

config={{
  ui: {
    categories: ["bug", "feature", "ux", "general"],
  },
}}

The selected category is included in the payload as category. All four categories are shown by default. To show only a subset:

config={{
  ui: {
    categories: ["bug", "feature"],  // only Bug and Feature pills
  },
}}

Set categories: [] to hide the pill selector entirely.

Conditional Display (showOn)

Restrict widget visibility to specific routes. By default the widget shows on all pages.

Route patterns

config={{
  ui: {
    showOn: ["/dashboard/*", "/app/*", "/settings"],
  },
}}

Patterns support exact matching or wildcard * suffix (matches any path starting with the prefix).

Custom predicate

config={{
  ui: {
    showOn: (pathname) => !pathname.startsWith("/admin"),
  },
}}

The widget automatically detects SPA navigation (pushState, replaceState, popstate) and re-evaluates on every route change.

Programmatic API

Control the widget programmatically via a React ref:

import { useRef } from "react";
import { BlocFeedWidget, type BlocFeedHandle } from "blocfeed";

function App() {
  const feedbackRef = useRef<BlocFeedHandle>(null);

  return (
    <>
      <button onClick={() => feedbackRef.current?.open()}>
        Report a Bug
      </button>

      <BlocFeedWidget
        ref={feedbackRef}
        blocfeed_id="bf_..."
      />
    </>
  );
}

BlocFeedHandle methods

| Method | Description | |--------|-------------| | open() | Open the widget (starts element picking) | | close() | Close the widget and reset state | | submit(message) | Submit feedback programmatically (returns Promise<SubmitResult>) | | isOpen | boolean — whether the widget is currently active |

Console & Network Diagnostics

Automatically capture console errors and failed network requests to include with every feedback submission:

config={{
  diagnostics: {
    console: true,               // capture console.error & console.warn
    consoleLevels: ["error", "warn"],  // which levels to capture
    consoleLimit: 20,            // max entries retained
    network: true,               // capture failed fetch & XHR requests
    networkLimit: 15,            // max entries retained
  },
}}

Captured data is attached to metadata._consoleLogs and metadata._networkErrors in the payload. Messages and stacks are truncated at 2KB.

Both fetch and XMLHttpRequest are intercepted, so apps using axios, jQuery, or other XHR-based libraries get full network error capture.

Network URL filtering (ignoreUrls)

By default, BlocFeed automatically filters out requests to analytics, tracking, and third-party service domains to keep diagnostics focused on relevant errors. The built-in blocklist includes 50+ domains:

| Category | Blocked domains | |----------|----------------| | BlocFeed | blocfeed.com | | Google Analytics / Ads | google-analytics.com, googletagmanager.com, analytics.google.com, googleads.g.doubleclick.net, googlesyndication.com, googleadservices.com, google.com/pagead, google.com/ads | | Facebook / Meta | facebook.net, facebook.com/tr, connect.facebook.net, graph.facebook.com | | Mixpanel | api.mixpanel.com, api-js.mixpanel.com | | Amplitude | api.amplitude.com, api2.amplitude.com | | Segment | api.segment.io, cdn.segment.com | | Hotjar | vars.hotjar.com, in.hotjar.com, script.hotjar.com | | Heap | heapanalytics.com | | FullStory | fullstory.com/s/fs.js, rs.fullstory.com | | PostHog | app.posthog.com, us.posthog.com, eu.posthog.com | | Intercom | api-iam.intercom.io, widget.intercom.io | | Sentry | sentry.io/api, browser.sentry-cdn.com | | Datadog | browser-intake-datadoghq.com | | Microsoft Clarity | clarity.ms | | HubSpot | js.hs-analytics.net, api.hubapi.com, forms.hubspot.com | | Plausible | plausible.io/api | | Crisp | client.crisp.chat | | Drift | js.driftt.com | | LogRocket | r.logrocket.com | | Pendo | app.pendo.io | | LaunchDarkly | events.launchdarkly.com, app.launchdarkly.com | | Grammarly | grammarly.com, gnar.grammarly.com, capi.grammarly.com | | LanguageTool | api.languagetool.org, api.languagetoolplus.com | | Google Fonts | fonts.googleapis.com, fonts.gstatic.com | | Cookie consent | cdn.cookielaw.org, consent.cookiebot.com | | Stripe.js | js.stripe.com, api.stripe.com/v1/tokens | | CAPTCHA | google.com/recaptcha, hcaptcha.com | | New Relic | bam.nr-data.net | | Smartlook | rec.smartlook.com | | Mouseflow | o2.mouseflow.com | | Lucky Orange | api.luckyorange.com | | Zendesk | static.zdassets.com, ekr.zdassets.com |

Each pattern is matched as a substring against the request URL (case-insensitive).

Add extra domains

Pass additional patterns alongside the built-in blocklist:

config={{
  diagnostics: {
    network: true,
    ignoreUrls: ["my-internal-analytics.com", "custom-tracker.io"],
  },
}}

Note: When you provide ignoreUrls, it replaces the built-in blocklist entirely. To keep the defaults and add your own patterns, you would need to include them manually — or simply omit ignoreUrls to use the built-in list as-is.

Capture everything (disable blocklist)

config={{
  diagnostics: {
    network: true,
    ignoreUrls: [],  // empty array disables all filtering
  },
}}

Headless diagnostics

import {
  installDiagnostics,
  uninstallDiagnostics,
  drainDiagnostics,
  clearDiagnostics,
} from "blocfeed/engine";

Security — Client-Side Secret Leak Detection

Detect accidentally exposed API keys, database credentials, and tokens in client-side code:

<BlocFeedWidget
  blocfeed_id="bf_..."
  config={{
    security: {
      secretScan: true,           // enable scanning (default when security config is present)
      scanTargets: ["hydration", "scripts", "meta", "dom"],  // what to scan
    },
  }}
/>

When secrets are detected, a dismissible warning banner appears in the widget, and findings are attached as metadata._securityFindings in every feedback submission. Secret values are never transmitted — only the first 6 characters + ***.

What gets scanned

| Target | What | Why | |--------|------|-----| | hydration | __NEXT_DATA__, __NUXT__ | Server-side props leaked to client | | scripts | Inline <script> tags | Hardcoded secrets in JavaScript | | meta | <meta> tag content attributes | Tokens in meta tags | | dom | data-api-key, data-secret, etc. | Keys in HTML attributes |

Built-in patterns (20+)

  • Supabase / Postgres: SUPABASE_SERVICE_ROLE_KEY, POSTGRES_PASSWORD, DATABASE_URL with embedded credentials
  • AWS: Access keys (AKIA...), secret keys, session tokens
  • Stripe: Secret keys (sk_live_..., sk_test_...), restricted keys
  • GitHub: PATs (ghp_), OAuth tokens (gho_), fine-grained tokens (github_pat_)
  • OpenAI / Anthropic: API keys
  • Infrastructure: Private keys, database URLs with credentials, SendGrid, Twilio
  • Generic: Any env var ending in _SECRET, _PASSWORD, _PRIVATE_KEY

Note: Known-public keys like SUPABASE_ANON_KEY and SUPABASE_URL are intentionally not flagged.

Custom patterns

Add app-specific secret formats:

config={{
  security: {
    customPatterns: [
      { name: "internal_api_key", pattern: /myapp_sk_[a-zA-Z0-9]{32}/ },
    ],
  },
}}

Headless secret scanning

import {
  runSecretScan,
  getSecurityFindings,
  clearSecurityFindings,
} from "blocfeed/engine";

runSecretScan({ secretScan: true });
// ... after scan completes (~100ms)
const snapshot = getSecurityFindings();
console.log(snapshot.findings);

Theme Customization

Override the widget's visual appearance via CSS variables:

config={{
  ui: {
    theme: {
      accentColor: "#12D393",           // buttons, highlights, focus rings
      panelBackground: "rgba(0,0,0,0.95)", // panel & trigger background
      panelForeground: "#ffffff",        // text color
      fontFamily: "Inter, sans-serif",   // font stack
      mode: "auto",                      // "dark" | "light" | "auto"
    },
  },
}}

Dark / Light Mode

The widget defaults to dark mode. Set theme.mode to change the color scheme:

| Mode | Behavior | |------|----------| | "dark" | Dark panel, light text (default) | | "light" | Light panel, dark text | | "auto" | Follows the user's system preference (prefers-color-scheme) and updates live |

Explicit panelBackground / panelForeground overrides take precedence over the mode defaults.

Retry & Transport

Configure retry behavior for unreliable networks:

config={{
  transport: {
    timeoutMs: 15000,   // 15s timeout per attempt
    maxAttempts: 3,      // retry up to 3 times
    backoffMs: 1000,     // 1s base backoff (exponential with jitter)
  },
}}

Offline Queue

When a submission fails due to a network error, the payload is automatically saved to localStorage and retried:

  • On the next page load (1s after controller initialization)
  • When the browser comes back online (online event)

Screenshots and video recordings are stripped from queued payloads to stay within localStorage limits. The queue holds up to 50 items (FIFO eviction).

Picking rules (ignore / filter)

Ignore a subtree (recommended)

<div data-blocfeed-ignore="true">Sensitive UI</div>

Ignore selectors / filter by predicate

picker: {
  ignoreSelectors: ["[data-private]", ".no-feedback"],
  isSelectable: (el) => el.tagName !== "HTML" && el.tagName !== "BODY"
}

Component name + test id in payload (triage)

selection.componentName

React component names are not available from the DOM by default. For a stable name, tag a component root:

export function CheckoutButton() {
  return (
    <button data-blocfeed-component="CheckoutButton" type="button">
      Checkout
    </button>
  );
}

BlocFeed will also attempt a best-effort component name in React/Next dev builds (no setup), but it may be missing/minified in production. In production, the fiber traversal is limited to 10 nodes (vs 80 in dev) to avoid wasting cycles on minified names.

selection.testId

BlocFeed extracts a best-effort testId from common attributes like data-testid, data-cy, etc.

Screenshots

Screenshots are uploaded efficiently using a two-phase flow:

  1. The text payload (metadata, selection, message) is sent first without screenshot data
  2. If the platform returns presigned upload URLs (Wasabi/S3), screenshots are uploaded directly to object storage
  3. If presigned URLs are unavailable, screenshots fall back to a secondary POST endpoint

Both element and full-page screenshots are supported and stored on the platform.

Video Recording

Let users record a short screen capture clip alongside their feedback. Uses the browser's Screen Capture API (getDisplayMedia) to record the current tab as WebM video.

<BlocFeedWidget
  blocfeed_id="bf_..."
  config={{
    recording: {
      enabled: true,              // default: false
      maxDurationMs: 30_000,      // default: 30s — auto-stops at this limit
      videoBitsPerSecond: 2_500_000, // default: 2.5 Mbps (~9 MB for 30s)
    },
  }}
/>

RecordingConfig options

| Option | Type | Default | Description | |--------|------|---------|-------------| | enabled | boolean | false | Show the Record button in the review phase | | maxDurationMs | number | 30000 | Maximum recording duration in ms. Auto-stops when reached | | mime | "video/webm" | "video/webm" | Output format (WebM is the only supported format) | | videoBitsPerSecond | number | 2500000 | Video bitrate. Lower = smaller files, faster uploads |

Bitrate & file size guide

| videoBitsPerSecond | 30s file size | Upload time (10 Mbps) | |---|---|---| | 1_000_000 (1 Mbps) | ~3.75 MB | ~3s | | 2_500_000 (2.5 Mbps) | ~9.4 MB | ~7s | | 5_000_000 (5 Mbps) | ~18.8 MB | ~15s |

Lower bitrates are fine for bug reproduction clips. 2.5 Mbps (default) gives crisp screen recordings.

How it works

  1. A Record button appears in the review phase (after element selection)
  2. Clicking it triggers the browser's screen sharing permission prompt
  3. A pulsing red dot + elapsed timer shows during recording
  4. Recording stops when the user clicks Stop, the max duration is reached, or the browser's "Stop sharing" button is clicked
  5. A video preview with playback controls appears — the user can remove it or submit
  6. On submit, the video is uploaded directly to object storage via a presigned URL

Browser support

Video recording requires getDisplayMedia and MediaRecorder with WebM support. The Record button is automatically hidden on unsupported browsers.

| Browser | Supported | |---------|-----------| | Chrome / Edge | Yes | | Firefox | Yes | | Safari 16+ | No (WebM not supported by MediaRecorder) |

Programmatic API

const ref = useRef<BlocFeedHandle>(null);

ref.current?.startRecording(); // start recording (must be in review phase)
ref.current?.stopRecording();  // stop recording

Localization

Override recording-related UI text via config.ui.strings:

strings: {
  recordButton: "Record",
  stopRecordButton: "Stop",
  recordingText: "Recording…",
  recordingDeniedText: "Screen recording permission was denied.",
  videoPreviewLabel: "Video preview",
  removeVideoButton: "Remove video",
}

Known limitations

The default screenshot engine (html-to-image) has known issues with:

  • Cross-origin images without permissive CORS headers
  • CSS clip-path and backdrop-filter
  • Some web fonts

Alternative screenshot adapter

For better compatibility, you can use modern-screenshot as a drop-in replacement:

npm install modern-screenshot
import { createModernScreenshotAdapter } from "blocfeed/engine";
import * as modernScreenshot from "modern-screenshot";

<BlocFeedWidget
  blocfeed_id="bf_..."
  config={{
    capture: {
      adapter: createModernScreenshotAdapter(modernScreenshot),
    },
  }}
/>

Convert to Blob

import { dataUrlToBlob } from "blocfeed/engine";

const blob = dataUrlToBlob(payload.screenshots?.element?.dataUrl ?? "");

Keyboard Accessibility

The widget supports full keyboard navigation:

  • Tab cycles through interactive elements in the feedback panel
  • Shift+Tab cycles backward
  • Escape cancels picking or closes the panel
  • Ctrl/Cmd+Enter submits feedback from the textarea
  • Custom shortcut — configurable via config.ui.shortcut (e.g., "mod+shift+f")
  • Focus is trapped within the panel when open
  • All interactive elements have aria-label attributes
  • Status messages use aria-live="polite" for screen reader announcements
  • Animations respect prefers-reduced-motion: reduce — all motion is disabled when the user prefers reduced motion

Client-Side Rate Limiting

A minimum 2-second interval is enforced between submissions to prevent accidental spam. Rapid submissions return a descriptive error.

Custom UI

If you don't want the default widget UI:

import { BlocFeedProvider, useBlocFeed } from "blocfeed";

function FeedbackButton() {
  const { start } = useBlocFeed();
  return <button onClick={start}>Give feedback</button>;
}

export function App() {
  return (
    <BlocFeedProvider blocfeed_id="bf_your_project_blocfeed_id">
      {/* your app */}
      <FeedbackButton />
    </BlocFeedProvider>
  );
}

Headless engine

import { createBlocFeedController } from "blocfeed/engine";

const controller = createBlocFeedController({ blocfeed_id: "bf_your_project_blocfeed_id" });

Offline queue utilities (headless)

import { enqueue, dequeueAll, clearQueue, getQueueSize } from "blocfeed/engine";

TypeScript

All types are exported from both entry points:

import type {
  BlocFeedConfig,
  BlocFeedHandle,
  BlocFeedUser,
  FeedbackCategory,
  FeedbackPayload,
  FeedbackApiResponse,
  BlocFeedState,
  SessionPhase,
  ThemeConfig,
  TransportConfig,
  TriggerStyle,
  WidgetPosition,
  RecordingConfig,
  VideoAsset,
  VideoIntent,
  VideoMime,
  SecurityConfig,
  SecurityFinding,
  SecuritySnapshot,
  // ... and more
} from "blocfeed";

Local development

cd blocfeed
npm test
npm run build
npm run playground:install
npm run playground:dev

License

MIT