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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@embeddable/sdk

v1.0.32

Published

A TypeScript/JavaScript SDK with React utilities and hooks for embeddable applications

Downloads

96

Readme

@embeddable/sdk

A TypeScript/JavaScript SDK with React utilities and hooks for embeddable applications. Built with tree shaking support and modern development practices.

npm version CI/CD codecov

Features

  • 🌳 Tree Shaking: Import only what you need
  • 📦 TypeScript: Full TypeScript support with type definitions
  • ⚛️ React Hooks: Custom hooks for common patterns
  • 🛠️ Utilities: Useful utility functions
  • 📱 Storage: Enhanced localStorage utilities
  • 🌐 API Client: Type-safe API client with error handling
  • 🧪 Well Tested: Comprehensive test coverage

Installation

npm install @embeddable/sdk
# or
yarn add @embeddable/sdk
# or
pnpm add @embeddable/sdk

Usage

Import Everything

import { useLocalStorage, debounce } from '@embeddable/sdk';

Import Specific Modules (Tree Shaking)

// Import only hooks
import { useLocalStorage } from '@embeddable/sdk/hooks';

// Import only utilities
import { debounce } from '@embeddable/sdk/utils';

Global Configuration

The SDK supports global configuration through the EmbeddableProvider context. This allows you to set a widgetId and version and mode once and access them throughout your application.

Setup

Wrap your application with the EmbeddableProvider:

import { EmbeddableProvider } from '@embeddable/sdk';
import type { EmbeddableConfig } from '@embeddable/sdk';

function App() {
  const config: EmbeddableConfig = {
    widgetId: 'my-widget-123',
    version: '1.0.0', // 'dev' | 'latest' | string
    mode: 'embeddable', // 'embeddable' | 'standalone' | 'preview'
    ignoreCache: false,
    lazyLoad: false,
    loader: true,
    _containerId: 'my-container-id',
    _shadowRoot: undefined,
  };

  return (
    <EmbeddableProvider config={config}>
      <YourAppComponents />
    </EmbeddableProvider>
  );
}

Using Global Configuration

Access the global configuration in any component:

import { useEmbeddableConfig, useApi } from '@embeddable/sdk';

function WidgetComponent() {
  const config = useEmbeddableConfig();

  return (
    <div>
      <h2>Widget: {config.widgetId}</h2>
      <p>Version: {config.version}</p>
      <p>Mode: {config.mode}</p>
      <p>Ignore Cache: {config.ignoreCache ? 'Yes' : 'No'}</p>
      <p>Lazy Load: {config.lazyLoad ? 'Yes' : 'No'}</p>
      <p>Show Loader: {config.loader ? 'Yes' : 'No'}</p>
      {/* Your widget content */}
    </div>
  );
}

API Reference

Hooks

useLocalStorage<T>(key: string, initialValue: T, options?: LocalStorageOptions)

A React hook for localStorage with state synchronization across tabs.

import { useLocalStorage } from '@embeddable/sdk/hooks'

function MyComponent() {
  const [user, setUser, removeUser] = useLocalStorage('user', { name: '', email: '' })

  return (
    <div>
      <input
        value={user.name}
        onChange={(e) => setUser(prev => ({ ...prev, name: e.target.value }))}
      />
      <button onClick={removeUser}>Clear</button>
    </div>
  )
}

useDebounce<T>(value: T, delay: number)

A React hook that debounces a value.

import { useDebounce } from '@embeddable/sdk/hooks'

function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('')
  const debouncedSearchTerm = useDebounce(searchTerm, 300)

  useEffect(() => {
    if (debouncedSearchTerm) {
      // Perform search
    }
  }, [debouncedSearchTerm])

  return <input onChange={(e) => setSearchTerm(e.target.value)} />
}

useEmbeddableConfig()

A React hook to access the global SDK configuration.

import { useEmbeddableConfig } from '@embeddable/sdk/hooks';

function MyWidget() {
  const config = useEmbeddableConfig();

  return (
    <div>
      <h3>Widget ID: {config.widgetId}</h3>
      <p>Version: {config.version}</p>
      <p>Mode: {config.mode}</p>
    </div>
  );
}

Note: This hook must be used within an EmbeddableProvider.

useFormSubmission<TPayload>(options?: FormSubmissionOptions)

A React hook for handling form submissions with loading states, error handling, and payload validation. Automatically uses the widget ID from the global configuration.

import { useFormSubmission } from '@embeddable/sdk/hooks';

// Define your form data type
interface ContactForm {
  name: string;
  email: string;
  message: string;
}

function ContactFormComponent() {
  const [formData, setFormData] = useState<ContactForm>({
    name: '',
    email: '',
    message: ''
  });

  const { submit, loading, error, success, reset } = useFormSubmission<ContactForm>({
    collectionName: 'contact-forms', // Optional, defaults to 'submissions'
    validatePayload: (payload) => {
      if (!payload.email) return 'Email is required';
      if (!payload.name) return 'Name is required';
      if (!payload.message) return 'Message is required';
      return null; // No validation errors
    }
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    const result = await submit(formData);
    if (result.success) {
      console.log('Form submitted successfully!', result.id);
      // Reset form or show success message
      setFormData({ name: '', email: '', message: '' });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Name"
        value={formData.name}
        onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
      />
      <input
        type="email"
        placeholder="Email"
        value={formData.email}
        onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
      />
      <textarea
        placeholder="Message"
        value={formData.message}
        onChange={(e) => setFormData(prev => ({ ...prev, message: e.target.value }))}
      />

      <button type="submit" disabled={loading}>
        {loading ? 'Submitting...' : 'Submit'}
      </button>

      {error && <p style={{ color: 'red' }}>{error}</p>}
      {success && <p style={{ color: 'green' }}>Message sent successfully!</p>}

      <button type="button" onClick={reset}>Reset Status</button>
    </form>
  );
}

Options:

  • collectionName?: string - The collection name for submissions (defaults to 'submissions')
  • validatePayload?: (payload: any) => string | null - Optional validation function

Returns:

  • submit: (payload: TPayload) => Promise<FormSubmissionResponse> - Function to submit the form
  • loading: boolean - Whether the submission is in progress
  • error: string | null - Error message if submission failed
  • success: boolean - Whether the submission was successful
  • reset: () => void - Function to reset the hook state

useVote(options?: VoteOptions)

A React hook for handling votes with loading states and error handling. Automatically uses the widget ID from the global configuration.

import { useVote } from '@embeddable/sdk/hooks';

function VotingComponent() {
  const { vote, loading, error, success, reset } = useVote({
    collectionName: 'product-poll' // Optional, defaults to 'votes'
  });

  const handleVote = async (optionId: string) => {
    const result = await vote(optionId);
    if (result.success) {
      console.log('Vote submitted!', result.id);
    }
  };

  return (
    <div>
      <h3>Which product do you prefer?</h3>

      <button
        onClick={() => handleVote('product-a')}
        disabled={loading}
      >
        Product A
      </button>

      <button
        onClick={() => handleVote('product-b')}
        disabled={loading}
      >
        Product B
      </button>

      <button
        onClick={() => handleVote('product-c')}
        disabled={loading}
      >
        Product C
      </button>

      {loading && <p>Submitting your vote...</p>}
      {error && <p style={{ color: 'red' }}>Error: {error}</p>}
      {success && <p style={{ color: 'green' }}>Thank you for voting!</p>}

      <button onClick={reset}>Reset</button>
    </div>
  );
}

// Example with dynamic voting options
function DynamicVotingComponent() {
  const { vote, loading, error, success } = useVote({
    collectionName: 'feature-requests'
  });

  const features = [
    { id: 'dark-mode', name: 'Dark Mode' },
    { id: 'mobile-app', name: 'Mobile App' },
    { id: 'api-access', name: 'API Access' },
  ];

  const handleFeatureVote = async (featureId: string, featureName: string) => {
    const result = await vote(featureId);
    if (result.success) {
      alert(`Thank you for voting for ${featureName}!`);
    }
  };

  return (
    <div>
      <h3>Vote for the next feature:</h3>
      {features.map(feature => (
        <button
          key={feature.id}
          onClick={() => handleFeatureVote(feature.id, feature.name)}
          disabled={loading}
          style={{
            margin: '5px',
            padding: '10px',
            opacity: loading ? 0.6 : 1
          }}
        >
          {feature.name}
        </button>
      ))}

      {loading && <p>Processing vote...</p>}
      {error && <p style={{ color: 'red' }}>{error}</p>}
      {success && <p style={{ color: 'green' }}>Vote recorded!</p>}
    </div>
  );
}

Options:

  • collectionName?: string - The collection name for votes (defaults to 'votes')

Returns:

  • vote: (voteFor: string) => Promise<VoteResponse> - Function to submit a vote
  • loading: boolean - Whether the vote submission is in progress
  • error: string | null - Error message if vote submission failed
  • success: boolean - Whether the vote was submitted successfully
  • reset: () => void - Function to reset the hook state

Note: Both useFormSubmission and useVote hooks must be used within an EmbeddableProvider as they automatically retrieve the widgetId from the global configuration.

useVoteAggregations(options?: UseVoteAggregationsOptions)

A React hook for fetching vote aggregations with loading states, error handling, and optional auto-refresh. Automatically uses the widget ID from the global configuration.

import { useVoteAggregations } from '@embeddable/sdk/hooks';

function VoteResultsComponent() {
  const { data, loading, error, refetch, reset } = useVoteAggregations({
    collectionName: 'product-poll', // Optional, defaults to 'votes'
    autoFetch: true, // Optional, defaults to true
    refetchInterval: 30000, // Optional, refetch every 30 seconds
  });

  if (loading) {
    return <div>Loading vote results...</div>;
  }

  if (error) {
    return (
      <div>
        <p style={{ color: 'red' }}>Error: {error}</p>
        <button onClick={refetch}>Retry</button>
      </div>
    );
  }

  if (!data) {
    return <div>No vote data available</div>;
  }

  return (
    <div>
      <h3>Vote Results</h3>

      <div>
        <p><strong>Total Votes:</strong> {data.summary.totalVotes}</p>
        <p><strong>Total Options:</strong> {data.summary.totalOptions}</p>
      </div>

      <div>
        {data.results.map((result) => (
          <div key={result._id} style={{ margin: '10px 0', padding: '10px', border: '1px solid #ccc' }}>
            <h4>{result._id}</h4>
            <p>Votes: {result.count}</p>
            {result.percentage && <p>Percentage: {result.percentage.toFixed(1)}%</p>}
            <div style={{ width: '100%', backgroundColor: '#e0e0e0', borderRadius: '4px' }}>
              <div
                style={{
                  width: `${result.percentage || 0}%`,
                  backgroundColor: '#4caf50',
                  height: '20px',
                  borderRadius: '4px',
                  transition: 'width 0.3s ease'
                }}
              />
            </div>
          </div>
        ))}
      </div>

      <button onClick={refetch}>Refresh Results</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

// Example with manual fetching
function ManualVoteResultsComponent() {
  const { data, loading, error, fetch } = useVoteAggregations({
    collectionName: 'feature-requests',
    autoFetch: false, // Don't fetch automatically
  });

  const handleLoadResults = async () => {
    const response = await fetch();
    if (response.success) {
      console.log('Results loaded:', response.data);
    }
  };

  return (
    <div>
      <button onClick={handleLoadResults} disabled={loading}>
        {loading ? 'Loading...' : 'Load Vote Results'}
      </button>

      {error && <p style={{ color: 'red' }}>{error}</p>}

      {data && (
        <div>
          <h3>Feature Request Votes</h3>
          <p>Total: {data.summary.totalVotes} votes</p>
          <ul>
            {data.results.map(result => (
              <li key={result._id}>
                {result._id}: {result.count} votes
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

// Example with real-time updates
function LiveVoteResultsComponent() {
  const { data, loading, error } = useVoteAggregations({
    collectionName: 'live-poll',
    refetchInterval: 5000, // Update every 5 seconds
  });

  return (
    <div>
      <h3>Live Poll Results {loading && '(Updating...)'}</h3>

      {error ? (
        <p style={{ color: 'red' }}>Failed to load results</p>
      ) : (
        <div>
          {data?.results.map(result => (
            <div key={result._id} style={{ display: 'flex', alignItems: 'center', margin: '5px 0' }}>
              <span style={{ minWidth: '100px' }}>{result._id}:</span>
              <div style={{ flex: 1, margin: '0 10px', backgroundColor: '#e0e0e0', borderRadius: '10px' }}>
                <div
                  style={{
                    width: `${result.percentage || 0}%`,
                    backgroundColor: '#2196f3',
                    height: '20px',
                    borderRadius: '10px',
                    transition: 'all 0.5s ease'
                  }}
                />
              </div>
              <span>{result.count} votes</span>
            </div>
          ))}
          <p style={{ marginTop: '10px', fontSize: '12px', color: '#666' }}>
            Last updated: {new Date().toLocaleTimeString()}
          </p>
        </div>
      )}
    </div>
  );
}

Options:

  • collectionName?: string - The collection name for votes (defaults to 'votes')
  • autoFetch?: boolean - Whether to automatically fetch data on mount (defaults to true)
  • refetchInterval?: number - Interval in milliseconds for automatic refetching (optional)

Returns:

  • data: VoteAggregationData | null - The aggregation data with results and summary
  • loading: boolean - Whether a request is in progress
  • error: string | null - Error message if request failed
  • fetch: () => Promise<VoteAggregationResponse> - Function to manually fetch aggregations
  • refetch: () => Promise<VoteAggregationResponse> - Alias for fetch function
  • reset: () => void - Function to reset the hook state

Data Structure:

interface VoteAggregationData {
  results: Array<{
    _id: string; // The option that was voted for
    count: number; // Number of votes for this option
    percentage?: number; // Percentage of total votes
  }>;
  summary: {
    totalVotes: number; // Total number of votes cast
    totalOptions: number; // Number of different options
    groupBy: string; // The field used for grouping
  };
}

Note: This hook must be used within an EmbeddableProvider as it automatically retrieves the widgetId from the global configuration.

useAI(options: UseAIOptions)

A React hook for handling AI API calls with loading states, error handling, and support for multiple AI platforms. Automatically uses the widget ID from the global configuration.

import { useAI } from '@embeddable/sdk/hooks';

function ChatComponent() {
  const { callAI, loading, error, success, reset } = useAI({
    platform: 'openai' // 'openai' | 'anthropic' | 'gemini'
  });

  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');

  const handleSendMessage = async () => {
    if (!input.trim()) return;

    const userMessage = { role: 'user', content: input };
    const newMessages = [...messages, userMessage];
    setMessages(newMessages);
    setInput('');

    const result = await callAI(input, messages);
    if (result.success && result.data) {
      setMessages(prev => [...prev, {
        role: 'assistant',
        content: result.data.response
      }]);

      console.log('Tokens used:', result.data.tokensUsed);
      console.log('Model used:', result.metadata?.model);
    }
  };

  return (
    <div>
      <div style={{ height: '400px', overflowY: 'auto', border: '1px solid #ccc', padding: '10px' }}>
        {messages.map((msg, index) => (
          <div key={index} style={{ marginBottom: '10px' }}>
            <strong>{msg.role}:</strong> {msg.content}
          </div>
        ))}
        {loading && <div>AI is thinking...</div>}
      </div>

      <div style={{ marginTop: '10px', display: 'flex', gap: '10px' }}>
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
          placeholder="Type your message..."
          style={{ flex: 1, padding: '8px' }}
          disabled={loading}
        />
        <button onClick={handleSendMessage} disabled={loading || !input.trim()}>
          Send
        </button>
      </div>

      {error && <p style={{ color: 'red', marginTop: '10px' }}>Error: {error}</p>}
      {success && <p style={{ color: 'green', marginTop: '10px' }}>Message sent successfully!</p>}

      <button onClick={reset} style={{ marginTop: '10px' }}>Reset Status</button>
    </div>
  );
}

Options:

  • platform: 'openai' | 'anthropic' | 'gemini' - The AI platform to use for API calls

Returns:

  • callAI: (prompt: string, history?: Array<ChatMessage>) => Promise<AIResponse> - Function to make AI API calls
  • loading: boolean - Whether an AI request is in progress
  • error: string | null - Error message if the request failed
  • success: boolean - Whether the last request was successful
  • reset: () => void - Function to reset the hook state

Data Structure:

interface ChatMessage {
  role: 'user' | 'assistant';
  content: string;
}

interface AIResponse {
  success: boolean;
  message?: string;
  error?: string;
  data?: {
    response: string;
    tokensUsed: number | null;
  };
  metadata?: {
    model: string | null;
    integrationKey: string;
    capabilityId: string;
    executedAt: string;
    [key: string]: any; // Additional metadata fields
  };
}

Utilities

debounce<T>(func: T, wait: number)

Creates a debounced function.

import { debounce } from '@embeddable/sdk/utils';

const debouncedSave = debounce(data => {
  // Save data
}, 500);

createApiClient(config: EmbeddableApiConfig)

Creates a type-safe API client.

import { createApiClient } from '@embeddable/sdk/utils';

const api = createApiClient({
  apiKey: 'your-api-key',
  baseUrl: 'https://api.example.com',
  debug: true,
});

const response = await api.get('/users');

storage

Enhanced localStorage utility with serialization support.

import { storage } from '@embeddable/sdk/utils';

// Basic usage
storage.set('user', { name: 'John', age: 30 });
const user = storage.get('user');

// With options
storage.set('data', complexObject, {
  prefix: 'myapp_',
  serialize: JSON.stringify,
  deserialize: JSON.parse,
});

Types

The SDK exports TypeScript types for better development experience:

import type {
  EmbeddableConfig,
  EmbeddableApiConfig,
  ApiResponse,
  LocalStorageOptions,
  FormSubmission,
  FormSubmissionResponse,
  FormSubmissionOptions,
  Vote,
  VoteResponse,
  VoteOptions,
  VoteAggregationResult,
  VoteAggregationSummary,
  VoteAggregationData,
  VoteAggregationResponse,
  ChatMessage,
  AIResponse,
  UseAIOptions,
} from '@embeddable/sdk';

Development

# Install dependencies
npm install

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Build the package
npm run build

# Run linter
npm run lint

# Fix linting issues
npm run lint:fix

# Format code with Prettier
npm run format

# Check code formatting
npm run format:check

# Type check
npm run type-check

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes using conventional commits (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

MIT © Embeddable Team

Changelog

See CHANGELOG.md for a list of changes.