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

@weirdfingers/boards

v0.9.13

Published

React hooks for Boards creative toolkit

Readme

@weirdfingers/boards

React hooks and components for building AI-powered creative applications with the Boards toolkit.

Overview

@weirdfingers/boards provides a React-first interface to the Boards creative toolkit. Build applications for AI-generated content (images, video, audio, text) using simple, composable hooks.

Key Features:

  • 🎣 Hooks-first design - Clean, React-native API without opinionated UI
  • 🔐 Pluggable auth - Works with Supabase, Clerk, Auth0, or custom providers
  • 📡 Real-time updates - SSE for generation progress, GraphQL subscriptions for live data
  • 🎨 Multi-modal support - Images, video, audio, text generation
  • 📦 Framework agnostic - Works with Next.js, Remix, Vite, or any React framework
  • 🔒 Type-safe - Full TypeScript support with generated GraphQL types

Installation

npm install @weirdfingers/boards react react-dom
# or
pnpm add @weirdfingers/boards react react-dom
# or
yarn add @weirdfingers/boards react react-dom

Peer Dependencies:

  • react ^18.0.0
  • react-dom ^18.0.0

Quick Start

1. Set up the provider

Wrap your application with BoardsProvider to configure the GraphQL client and auth:

import { BoardsProvider, NoAuthProvider } from '@weirdfingers/boards';

function App() {
  const authProvider = new NoAuthProvider(); // For development only

  return (
    <BoardsProvider
      apiUrl="http://localhost:8088"
      authProvider={authProvider}
    >
      <YourApp />
    </BoardsProvider>
  );
}

2. Use hooks in your components

import { useBoards, useGeneration } from '@weirdfingers/boards';

function BoardsPage() {
  const { boards, loading, createBoard } = useBoards();
  const { submit, progress, isGenerating } = useGeneration();

  const handleGenerate = async (boardId: string) => {
    await submit({
      boardId,
      model: 'flux-1-schnell',
      artifactType: 'IMAGE',
      inputs: {
        prompt: 'A serene mountain landscape at sunset',
      },
    });
  };

  if (loading) return <div>Loading boards...</div>;

  return (
    <div>
      {boards.map((board) => (
        <div key={board.id}>
          <h2>{board.title}</h2>
          <button onClick={() => handleGenerate(board.id)}>
            Generate Image
          </button>
        </div>
      ))}

      {progress && (
        <div>Progress: {progress.progress}%</div>
      )}
    </div>
  );
}

API Reference

Provider

<BoardsProvider>

Main provider component that sets up GraphQL client, auth, and configuration.

Props:

| Prop | Type | Required | Description | |------|------|----------|-------------| | apiUrl | string | Yes | Base URL for the backend API (e.g., http://localhost:8088) | | authProvider | BaseAuthProvider | Yes | Authentication provider instance | | graphqlUrl | string | No | GraphQL endpoint URL (defaults to ${apiUrl}/graphql) | | subscriptionUrl | string | No | WebSocket URL for GraphQL subscriptions | | tenantId | string | No | Tenant ID for multi-tenant deployments |

<BoardsProvider
  apiUrl="https://api.example.com"
  authProvider={authProvider}
  tenantId="my-tenant"
>
  {children}
</BoardsProvider>

Hooks

useBoards(options?)

Manage multiple boards - fetch, create, delete, and search.

Options:

interface UseBoardsOptions {
  limit?: number;   // Default: 50
  offset?: number;  // Default: 0
}

Returns:

interface BoardsHook {
  boards: Board[];
  loading: boolean;
  error: Error | null;

  // Operations
  createBoard: (data: CreateBoardInput) => Promise<Board>;
  deleteBoard: (boardId: string) => Promise<void>;
  searchBoards: (query: string) => Promise<Board[]>;
  refresh: () => Promise<void>;

  // Search state
  setSearchQuery: (query: string) => void;
  searchQuery: string;
}

Example:

function BoardsList() {
  const { boards, loading, createBoard, deleteBoard } = useBoards({ limit: 20 });

  const handleCreate = async () => {
    await createBoard({
      title: 'My New Board',
      description: 'A board for my creative projects',
    });
  };

  return (
    <div>
      <button onClick={handleCreate}>Create Board</button>
      {boards.map((board) => (
        <div key={board.id}>
          <h3>{board.title}</h3>
          <button onClick={() => deleteBoard(board.id)}>Delete</button>
        </div>
      ))}
    </div>
  );
}

useBoard(boardId)

Manage a single board - update, delete, and handle members/permissions.

Returns:

interface BoardHook {
  board: Board | null;
  members: BoardMember[];
  permissions: BoardPermissions;
  loading: boolean;
  error: Error | null;

  // Board operations
  updateBoard: (updates: Partial<UpdateBoardInput>) => Promise<Board>;
  deleteBoard: () => Promise<void>;
  refresh: () => Promise<void>;

  // Member management
  addMember: (email: string, role: MemberRole) => Promise<BoardMember>;
  removeMember: (memberId: string) => Promise<void>;
  updateMemberRole: (memberId: string, role: MemberRole) => Promise<BoardMember>;

  // Sharing (placeholder - backend implementation pending)
  generateShareLink: (options: ShareLinkOptions) => Promise<ShareLink>;
  revokeShareLink: (linkId: string) => Promise<void>;
}

Example:

function BoardDetail({ boardId }: { boardId: string }) {
  const {
    board,
    permissions,
    updateBoard,
    addMember,
  } = useBoard(boardId);

  const handleUpdate = async () => {
    await updateBoard({
      title: 'Updated Title',
      description: 'Updated description',
    });
  };

  const handleAddMember = async (email: string) => {
    await addMember(email, 'EDITOR');
  };

  return (
    <div>
      <h1>{board?.title}</h1>
      {permissions.canEdit && (
        <button onClick={handleUpdate}>Update Board</button>
      )}
      {permissions.canAddMembers && (
        <button onClick={() => handleAddMember('[email protected]')}>
          Add Member
        </button>
      )}
    </div>
  );
}

useGeneration()

Submit AI generation requests and track progress in real-time via Server-Sent Events (SSE).

Returns:

interface GenerationHook {
  // Current generation state
  progress: GenerationProgress | null;
  result: GenerationResult | null;
  error: Error | null;
  isGenerating: boolean;

  // Operations
  submit: (request: GenerationRequest) => Promise<string>;
  cancel: (jobId: string) => Promise<void>;
  retry: (jobId: string) => Promise<void>;

  // History
  history: GenerationResult[];
  clearHistory: () => void;
}

Example:

function ImageGenerator({ boardId }: { boardId: string }) {
  const { submit, progress, result, isGenerating, cancel } = useGeneration();
  const [prompt, setPrompt] = useState('');

  const handleGenerate = async () => {
    const jobId = await submit({
      boardId,
      model: 'flux-1-schnell',
      artifactType: 'IMAGE',
      inputs: {
        prompt,
        steps: 4,
        guidance: 3.5,
      },
      options: {
        priority: 'normal',
      },
    });
    console.log('Generation started:', jobId);
  };

  return (
    <div>
      <input
        value={prompt}
        onChange={(e) => setPrompt(e.target.value)}
        placeholder="Enter your prompt..."
      />
      <button onClick={handleGenerate} disabled={isGenerating}>
        Generate
      </button>

      {progress && (
        <div>
          <p>Status: {progress.status}</p>
          <p>Progress: {progress.progress}%</p>
          {progress.message && <p>{progress.message}</p>}
          {isGenerating && <button onClick={() => cancel(progress.jobId)}>Cancel</button>}
        </div>
      )}

      {result && (
        <div>
          <h3>Generation Complete!</h3>
          {result.artifacts.map((artifact) => (
            <img key={artifact.id} src={artifact.url} alt="Generated" />
          ))}
        </div>
      )}
    </div>
  );
}

useGenerators()

Fetch available AI generators and their input schemas.

Returns:

interface GeneratorsHook {
  generators: Generator[];
  loading: boolean;
  error: Error | null;

  // Utilities
  getGenerator: (name: string) => Generator | undefined;
  refresh: () => Promise<void>;
}

interface Generator {
  name: string;
  displayName: string;
  description: string;
  artifactType: ArtifactType;
  version: string;
  inputSchema: JSONSchema7;  // JSON Schema for inputs
  enabled: boolean;
  creditCost: number;
  estimatedDuration: number;
  maxDuration: number;
  metadata: Record<string, unknown>;
}

Example:

function GeneratorSelector() {
  const { generators, loading, getGenerator } = useGenerators();

  const fluxGenerator = getGenerator('flux-1-schnell');

  return (
    <div>
      <h2>Available Generators</h2>
      {generators
        .filter((g) => g.enabled)
        .map((generator) => (
          <div key={generator.name}>
            <h3>{generator.displayName}</h3>
            <p>{generator.description}</p>
            <p>Cost: {generator.creditCost} credits</p>
            <p>Type: {generator.artifactType}</p>
          </div>
        ))}
    </div>
  );
}

useAuth()

Access authentication state and user information.

Returns:

interface UseAuthReturn {
  user: User | null;
  loading: boolean;
  error: Error | null;

  // Auth operations
  getToken: () => Promise<string | null>;
  signOut: () => Promise<void>;
}

interface User {
  id: string;
  email: string;
  displayName: string;
  avatarUrl?: string;
  createdAt: string;
}

Example:

function UserProfile() {
  const { user, loading, signOut } = useAuth();

  if (loading) return <div>Loading...</div>;
  if (!user) return <div>Not authenticated</div>;

  return (
    <div>
      <h2>Welcome, {user.displayName}</h2>
      <p>{user.email}</p>
      <button onClick={signOut}>Sign Out</button>
    </div>
  );
}

Authentication

The package supports multiple auth providers through a plugin system.

Development (No Auth)

For local development and testing:

import { NoAuthProvider } from '@weirdfingers/boards';

const authProvider = new NoAuthProvider();

Custom Auth Provider

Implement BaseAuthProvider for custom authentication:

import { BaseAuthProvider, AuthState } from '@weirdfingers/boards';

class CustomAuthProvider extends BaseAuthProvider {
  async initialize(): Promise<void> {
    // Initialize your auth system
  }

  async getAuthState(): Promise<AuthState> {
    // Return current auth state
    return {
      isAuthenticated: true,
      userId: 'user-id',
      getToken: async () => 'your-jwt-token',
    };
  }

  async signOut(): Promise<void> {
    // Handle sign out
  }
}

Auth Adapter Packages (Coming Soon)

Dedicated packages for popular auth providers:

  • @weirdfingers/boards-auth-supabase - Supabase authentication
  • @weirdfingers/boards-auth-clerk - Clerk authentication
  • @weirdfingers/boards-auth-auth0 - Auth0 authentication

TypeScript Support

This package is written in TypeScript and includes full type definitions. All hooks and components are fully typed:

import type {
  Board,
  Generator,
  GenerationRequest,
  GenerationProgress,
  BoardRole,
  ArtifactType,
} from '@weirdfingers/boards';

// Types are automatically inferred
const { boards } = useBoards(); // boards: Board[]

Examples

Complete Application Setup

// App.tsx
import { BoardsProvider, NoAuthProvider } from '@weirdfingers/boards';
import { BoardsPage } from './BoardsPage';

const authProvider = new NoAuthProvider();

export function App() {
  return (
    <BoardsProvider
      apiUrl={import.meta.env.VITE_API_URL || 'http://localhost:8088'}
      authProvider={authProvider}
    >
      <BoardsPage />
    </BoardsProvider>
  );
}

// BoardsPage.tsx
import { useBoards, useGeneration } from '@weirdfingers/boards';

export function BoardsPage() {
  const { boards, loading, createBoard } = useBoards();
  const { submit, progress, isGenerating } = useGeneration();

  const handleCreateBoard = async () => {
    const board = await createBoard({
      title: 'My Creative Board',
      description: 'A space for AI-generated art',
    });
    console.log('Created board:', board.id);
  };

  const handleGenerate = async (boardId: string) => {
    await submit({
      boardId,
      model: 'flux-1-schnell',
      artifactType: 'IMAGE',
      inputs: {
        prompt: 'A futuristic cityscape at night',
        steps: 4,
        guidance: 3.5,
      },
    });
  };

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <h1>My Boards</h1>
      <button onClick={handleCreateBoard}>Create New Board</button>

      <div className="boards-grid">
        {boards.map((board) => (
          <div key={board.id}>
            <h2>{board.title}</h2>
            <p>{board.description}</p>
            <p>Generations: {board.generationCount}</p>
            <button onClick={() => handleGenerate(board.id)}>
              Generate Image
            </button>
          </div>
        ))}
      </div>

      {progress && (
        <div className="progress-bar">
          <p>Generating: {progress.progress}%</p>
          <p>{progress.message}</p>
        </div>
      )}
    </div>
  );
}

Handling Real-time Progress

import { useGeneration } from '@weirdfingers/boards';
import { useEffect, useState } from 'react';

function ProgressTracker() {
  const { submit, progress, result, error } = useGeneration();
  const [logs, setLogs] = useState<string[]>([]);

  useEffect(() => {
    if (progress?.message) {
      setLogs((prev) => [...prev, `${progress.status}: ${progress.message}`]);
    }
  }, [progress]);

  const handleGenerate = async () => {
    setLogs([]);
    await submit({
      boardId: 'board-id',
      model: 'flux-1-schnell',
      artifactType: 'IMAGE',
      inputs: { prompt: 'A magical forest' },
    });
  };

  return (
    <div>
      <button onClick={handleGenerate}>Generate</button>

      <div className="logs">
        {logs.map((log, i) => (
          <div key={i}>{log}</div>
        ))}
      </div>

      {result && (
        <div>
          <h3>Complete!</h3>
          <p>Time: {result.performance.totalTime}ms</p>
          <p>Cost: {result.credits.cost} credits</p>
        </div>
      )}

      {error && <div className="error">{error.message}</div>}
    </div>
  );
}

Related Packages

Documentation

For comprehensive documentation, guides, and tutorials, visit:

docs.weirdfingers.com

Contributing

Contributions are welcome! Please see the main repository for:

License

MIT License - see LICENSE for details.

Support

Community & Social

Join the Weirdfingers community:


Built with ❤️ by the Weirdfingers team