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

@shiftengineering/folio

v0.1.42

Published

A React component library for embedding and interacting with Folio documents, projects, and files in your application.

Readme

@shiftengineering/folio

A React component library for embedding and interacting with Folio documents, projects, and files in your application.

Installation

# Using npm
npm install @shiftengineering/folio

# Using yarn
yarn add @shiftengineering/folio

# Using pnpm
pnpm add @shiftengineering/folio

Usage

This package exports three main features:

  1. FolioProvider - Context provider for Folio connections
  2. FolioEmbed - React component to embed Folio in an iframe
  3. React hooks for interacting with Folio data:
    • useFolioProjects - Get projects data
    • useFolioFiles - Get files for a project
    • useAddFolioProject - Create new projects
    • useAddFolioFiles - Add files to a project
    • useAddFolioDirectoriesWithFiles - Add directories with files to a project
    • useFolioUserMetadata - Get and update user metadata

Secure Token Handling

By default, the component uses a secure token passing mechanism via postMessage instead of passing the JWT token as a URL query parameter. This ensures your token is not visible in network logs or browser history.

The token is passed securely as follows:

  1. The iframe loads without the token in the URL
  2. When the iframe is ready, it requests the token from the parent via postMessage
  3. The parent application responds with the token, which is then used for API requests

If you need backward compatibility with older versions, you can set the passTokenInQueryParam property to true on the FolioProvider:

<FolioProvider
  host="http://your-folio-server.com"
  port={5174}
  token={token}
  passTokenInQueryParam={true} // Legacy mode: passes token in URL (less secure)
>
  <App />
</FolioProvider>

Basic Setup

First, wrap your application with the FolioProvider:

import { FolioProvider } from "@shiftengineering/folio";
import App from "./App";

// The token must be a valid JWT that the Folio backend is configured to accept
const token = "your-jwt-auth-token";

// Optional user metadata to personalize AI responses
const userMetadata = {
  role: "Sales Representative",
  industry: "Healthcare",
};

ReactDOM.render(
  <FolioProvider
    host="http://your-folio-server.com"
    port={5174}
    token={token}
    userMetadata={userMetadata}
  >
    <App />
  </FolioProvider>,
  document.getElementById("root"),
);

Embedding Folio

Use the FolioEmbed component to embed Folio in your application:

import { FolioEmbed } from "@shiftengineering/folio";

function MyFolioPage() {
  return (
    <div className="folio-page">
      <h1>My Folio Documents</h1>
      <FolioEmbed
        width="100%"
        height="800px"
        className="my-custom-class"
        style={{ border: "1px solid #ccc" }}
      />
    </div>
  );
}

Working with Folio Data

Use the Folio hooks to get and manipulate Folio data:

import {
  useFolioProjects,
  useFolioFiles,
  useAddFolioProject,
  useAddFolioFiles,
  useAddFolioDirectoriesWithFiles,
  useFolioUserMetadata,
  type DirectoryEntry,
  type MetadataValue,
} from "@shiftengineering/folio";
import { useState } from "react";

function FolioProjectManager() {
  const [selectedProjectId, setSelectedProjectId] = useState(null);

  // Get projects with loading and error states
  const {
    projects,
    isLoading: isProjectsLoading,
    error: projectsError,
  } = useFolioProjects();

  // Get files for the selected project
  const { files, isLoading: isFilesLoading } = useFolioFiles(selectedProjectId);

  // Get and update user metadata
  const {
    metadata: userMetadata,
    updateMetadata,
    isLoading: isMetadataLoading
  } = useFolioUserMetadata();

  // Add a new project
  const { addProject, isAdding: isCreatingProject } = useAddFolioProject();

  // Add files to a project
  const { addFiles, isAdding: isAddingFiles } =
    useAddFolioFiles(selectedProjectId);

  // Add directories with files to a project
  const { addDirectoriesWithFiles, isAdding: isAddingDirectory } =
    useAddFolioDirectoriesWithFiles(selectedProjectId);

  const handleCreateProject = () => {
    addProject("My New Project");
  };

  const handleUpdateUserMetadata = () => {
    updateMetadata({
      role: "Project Manager",
      industry: "Finance",
      interestedIn: "State contracts"
    });
  };

  const handleAddFile = () => {
    addFiles([{
      blobUrl: "/path/to/file.pdf",
      name: "My Document.pdf",
      userProvidedId: "doc-123" // Required unique identifier for this file
    }]);
  };

  const handleAddSingleDirectory = () => {
    // Create metadata with nested structure
    const metadata = {
      category: "reports",
      details: {
        owner: "John Doe",
        department: "Finance",
        tags: ["important", "quarterly"],
      },
      status: {
        reviewed: true,
        approvalDate: "2023-10-15"
      }
    };

    const directoryEntry: DirectoryEntry = {
      directoryName: "My Documents",
      directoryMetadata: metadata,
      files: [
        {
          blobUrl: "/path/to/file1.pdf",
          name: "Document 1.pdf",
          userProvidedId: "doc-456" // Required unique identifier for this file
        },
        {
          blobUrl: "/path/to/file2.pdf",
          name: "Document 2.pdf",
          userProvidedId: "doc-789" // Required unique identifier for this file
        },
      ],
    };

    addDirectoriesWithFiles(directoryEntry);
  };

  const handleAddMultipleDirectories = () => {
    // Create metadata for each directory with nested structures
    const metadata1 = {
      category: "reports",
      details: {
        owner: "Jane Smith",
        department: "Accounting",
        tags: ["quarterly", "financial"]
      }
    };

    const metadata2 = {
      category: "contracts",
      details: {
        owner: "Legal Team",
        priority: "high",
        clients: ["Acme Inc", "Globex Corp"]
      },
      approvalChain: {
        legalApproved: true,
        executiveApproved: false
      }
    };

    const directories: DirectoryEntry[] = [
      {
        directoryName: "Reports",
        directoryMetadata: metadata1,
        files: [
          {
            blobUrl: "/path/to/report1.pdf",
            name: "Report 1.pdf",
            userProvidedId: "report-1" // Required unique identifier for this file
          },
          {
            blobUrl: "/path/to/report2.pdf",
            name: "Report 2.pdf",
            userProvidedId: "report-2" // Required unique identifier for this file
          },
        ],
      },
      {
        directoryName: "Contracts",
        directoryMetadata: metadata2,
        files: [
          {
            blobUrl: "/path/to/contract1.pdf",
            name: "Contract 1.pdf",
            userProvidedId: "contract-1" // Required unique identifier for this file
          },
          {
            blobUrl: "/path/to/contract2.pdf",
            name: "Contract 2.pdf",
            userProvidedId: "contract-2" // Required unique identifier for this file
          },
        ],
      },
    ];

    addDirectoriesWithFiles(directories);
  };

  if (isProjectsLoading) return <div>Loading projects...</div>;
  if (projectsError) return <div>Error: {projectsError.message}</div>;

  return (
    <div>
      <button onClick={handleCreateProject} disabled={isCreatingProject}>
        {isCreatingProject ? "Creating..." : "Create New Project"}
      </button>

      <h2>Your Projects</h2>
      <ul>
        {projects.map((project) => (
          <li
            key={project.id}
            onClick={() => setSelectedProjectId(project.id)}
            style={{
              fontWeight: project.id === selectedProjectId ? "bold" : "normal",
            }}
          >
            {project.name}
          </li>
        ))}
      </ul>

      {selectedProjectId && (
        <>
          <h2>Project Files</h2>
          <button onClick={handleAddFile} disabled={isAddingFiles}>
            {isAddingFiles ? "Adding..." : "Add File"}
          </button>
          <button onClick={handleAddSingleDirectory} disabled={isAddingDirectory}>
            {isAddingDirectory ? "Adding..." : "Add Directory with Files"}
          </button>
          <button onClick={handleAddMultipleDirectories} disabled={isAddingDirectory}>
            {isAddingDirectory ? "Adding..." : "Add Multiple Directories"}
          </button>

          {isFilesLoading ? (
            <div>Loading files...</div>
          ) : (
            <ul>
              {files.map((file) => (
                <li key={file.id}>
                  {file.name}
                  {file.userProvidedId && <small> (ID: {file.userProvidedId})</small>}
                </li>
              ))}
            </ul>
          )}
        </>
      )}
    </div>
  );
}

Google Analytics 4 Integration

Folio supports analytics event tracking that can be used with Google Analytics 4 or any other analytics provider. This feature enables tracking key user interactions and provides visibility into how users engage with the application.

Configuration

For Google Analytics 4

To enable built-in Google Analytics 4 tracking, add the measurement ID to your environment variables provided to the docker container:

VITE_GA4_MEASUREMENT_ID=G-XXXXXXXXXX

When this environment variable is present, Folio will automatically initialize GA4 and send events to Google Analytics.

Server Environment: Configuration

If you're hosting the Folio backend yourself, you can configure various aspects of the server via environment variables.

Langfuse Observability

Folio supports Langfuse for tracing and observability of OpenAI API calls. To enable Langfuse tracing, add the following environment variables:

LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_BASE_URL=https://cloud.langfuse.com  # or https://us.cloud.langfuse.com for US region

When these variables are set, all OpenAI chat and embedding requests will be traced in Langfuse, giving you visibility into:

  • Token usage and costs
  • Request latency
  • User sessions (when using Clerk authentication)
  • Error tracking

If these variables are not set, the application will run normally without tracing.

Embedding Configuration

You can control the embeddings API behavior on a per-container basis via environment variables:

Embedding Request Rate Limit

EMBEDDING_RATE_LIMIT_PER_MINUTE=5000
  • What it does: Caps embedding requests at the specified number per minute for that container.
  • Default: 5000 if not set.
  • Scaling: In multi-container setups, set this per container to your desired per-container limit.

Embedding Token Rate Limit

EMBEDDING_TOKENS_PER_MINUTE=5000000
  • What it does: Caps the total number of tokens processed for embeddings per minute for that container.
  • Default: 5000000 (5 million) if not set.
  • Note: This helps manage costs and avoid hitting provider token-based rate limits.

Embedding Batch Size

EMBEDDING_MAX_BATCH_SIZE=100
  • What it does: Sets the maximum number of items to include in a single embedding batch request.
  • Default: 100 if not set.
  • Note: Larger batches are more efficient but may hit size limits with some providers.

Example with Docker Compose (build args):

services:
  folio:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        EMBEDDING_RATE_LIMIT_PER_MINUTE: ${EMBEDDING_RATE_LIMIT_PER_MINUTE:-5000}
        EMBEDDING_TOKENS_PER_MINUTE: ${EMBEDDING_TOKENS_PER_MINUTE:-5000000}
        EMBEDDING_MAX_BATCH_SIZE: ${EMBEDDING_MAX_BATCH_SIZE:-100}

For Any Analytics Provider

Important: Even if you don't configure GA4, you can still capture all analytics events by providing the onAnalyticsEvent callback to the FolioProvider. This gives you complete flexibility to use any analytics provider of your choice.

Events Tracked

The following events are tracked automatically:

  1. page_view - When a user navigates to a new page
  2. chat_sent - When a user sends a chat message (includes the query)
  3. highlight - When a user creates a highlight (includes file path and selection length)
  4. add_to_chat - When a user adds content to chat (includes file path and snippet size)
  5. extract - When content is extracted (includes file path and extractor type)
  6. switch_project - When a user switches between projects
  7. file_view - When a user views a file

Analytics Event Structure

All analytics events follow this structure:

export type AnalyticsEvent =
  | { name: "page_view"; data: { pathname: string; projectId?: string } }
  | { name: "chat_sent"; data: { query: string } }
  | { name: "highlight"; data: { filePath: string; selectionLength: number } }
  | { name: "add_to_chat"; data: { filePath: string; snippetSize: number } }
  | { name: "extract"; data: { filePath: string; extractor: string } }
  | {
      name: "switch_project";
      data: { fromProjectId: string; toProjectId: string };
    }
  | { name: "file_view"; data: { filePath: string } };

Each event has:

  • A name property identifying the event type
  • A data object with event-specific parameters

Host Integration

If you're embedding Folio in your application, you can access the analytics event stream by providing the onAnalyticsEvent callback to the FolioProvider:

import { FolioProvider, AnalyticsEvent } from "@shiftengineering/folio";

function YourApp() {
  const handleAnalyticsEvent = (event: AnalyticsEvent) => {
    // Forward to your own analytics system or process the data
    console.log(`Folio event: ${event.name}`, event.data);

    // Example: Send to Google Analytics
    if (window.gtag) {
      window.gtag('event', event.name, event.data);
    }

    // Example: Send to Mixpanel
    if (window.mixpanel) {
      window.mixpanel.track(event.name, event.data);
    }

    // Example: Send to custom analytics endpoint
    fetch('https://your-analytics-api.com/track', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(event)
    });
  };

  return (
    <FolioProvider
      host="http://your-folio-server.com"
      port={5174}
      token="your-auth-token"
      onAnalyticsEvent={handleAnalyticsEvent}
    >
      <YourAppContent />
    </FolioProvider>
  );
}

Alternatively, you can listen to the raw custom event directly:

window.addEventListener("folio-analytics", (event) => {
  // Access the event data from event.detail
  const { name, data } = event.detail;

  // Forward to your own analytics system or process the data
  console.log(`Folio event: ${name}`, data);
});

Both approaches allow host applications to consume the same events regardless of whether GA4 is configured, enabling integration with any analytics service or custom tracking solution.

Deep Linking

Folio supports deep linking for directories, allowing users to navigate back to the original platform when Folio is embedded in an iframe. This feature is useful when you want to provide a seamless experience between your platform and Folio.

Note: Currently, only directories support deep linking. Individual files do not have deep link functionality.

Configuration

Deep linking is controlled by the VITE_ENABLE_DIRECT_DEEPLINKS environment variable, which must be provided to the container at runtime. Set this to true to enable the feature.

How It Works

When a directory is created with a userProvidedId (using the useAddFolioDirectoriesWithFiles hook), a deep link button (external link icon) will appear next to the directory in:

  • The file list sidebar
  • The directory viewer page

When clicked, this button emits a deep link event containing:

  • userProvidedId - The unique identifier that was provided when the directory was created
  • metadata - The complete metadata object that was originally provided when creating the directory

Deep Link Event Structure

export type DeepLinkEvent = {
  userProvidedId: string;
  metadata: Record<string, unknown> | null;
};

Host Integration

To handle deep link events in your host application, provide the onDeepLinkEvent callback to the FolioProvider:

import { FolioProvider, DeepLinkEvent } from "@shiftengineering/folio";

function YourApp() {
  const handleDeepLinkEvent = (event: DeepLinkEvent) => {
    // Navigate back to the original platform
    console.log(`Deep link clicked:`, event);
    console.log(`User Provided ID: ${event.userProvidedId}`);
    console.log(`Metadata:`, event.metadata);

    // Example: Navigate to a specific page in your platform
    if (event.userProvidedId) {
      window.location.href = `/records/${event.userProvidedId}`;
    }

    // Example: Use metadata to construct a more complex URL
    if (event.metadata?.recordType === "contract") {
      window.location.href = `/contracts/${event.userProvidedId}`;
    }
  };

  return (
    <FolioProvider
      host="http://your-folio-server.com"
      port={5174}
      token="your-auth-token"
      onDeepLinkEvent={handleDeepLinkEvent}
    >
      <YourAppContent />
    </FolioProvider>
  );
}

API Reference

FolioProvider

Context provider that manages Folio application connection settings.

| Prop | Type | Default | Description | | ----------------------- | ------------------------------- | -------------------- | ------------------------------------------------------------------------------------ | | host | string | 'http://localhost' | Host for the Folio API and iframe | | port | number | 5174 | Port for the Folio API and iframe | | token | string | - | JWT authentication token that the Folio backend is configured to accept | | userMetadata | Record<string, MetadataValue> | - | Optional metadata for the current user that will be used to personalize AI responses | | onAnalyticsEvent | (event: AnalyticsEvent) => void | - | Optional callback for handling analytics events from Folio | | onDeepLinkEvent | (event: DeepLinkEvent) => void | - | Optional callback for handling deep link events when users click directory links | | passTokenInQueryParam | boolean | false | Whether to pass the token in URL (legacy, less secure) instead of using postMessage |

FolioEmbed

React component to embed Folio in an iframe.

| Prop | Type | Default | Description | | ------------- | ---------------- | ------------------------------------------------------------------- | ----------------------------------------------- | | width | string | number | '100%' | Width of the iframe | | height | string | number | '100vh' | Height of the iframe | | allow | string | 'camera; microphone; clipboard-read; clipboard-write; fullscreen' | Allow attributes for the iframe | | style | object | {} | Additional styles for the iframe container | | className | string | '' | Additional class names for the iframe container | | iframeProps | object | {} | Additional props to pass to the iframe |

Hooks

useFolioProjects()

Hook for getting all projects for the current user.

| Return Property | Type | Description | | --------------- | -------------------- | ------------------------------------- | | projects | FolioProject[] | Array of projects | | isLoading | boolean | Whether projects are being loaded | | isError | boolean | Whether an error occurred | | error | Error \| null | Error object if an error occurred | | refetch | () => Promise<...> | Function to manually refetch projects |

useFolioFiles(projectId?: number)

Hook for getting all files for a specific project.

| Return Property | Type | Description | | --------------- | -------------------- | ---------------------------------- | | files | FolioFile[] | Array of files | | isLoading | boolean | Whether files are being loaded | | isError | boolean | Whether an error occurred | | error | Error \| null | Error object if an error occurred | | refetch | () => Promise<...> | Function to manually refetch files |

useAddFolioProject()

Hook for adding a new project.

| Return Property | Type | Description | | ----------------- | ----------------------------------------- | -------------------------------------- | | addProject | (name: string) => void | Function to add a project | | addProjectAsync | (name: string) => Promise<FolioProject> | Async version returning a promise | | isAdding | boolean | Whether a project is being added | | isError | boolean | Whether an error occurred | | error | Error \| null | Error object if an error occurred | | newProject | FolioProject \| undefined | The newly created project if available |

useAddFolioFiles(projectId?: number)

Hook for adding files to a project. Files are always created at the root level (parentId = null) and are not directories.

| Return Property | Type | Description | | --------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------- | | addFiles | (files: { blobUrl: string; name: string; userProvidedId: string }[]) => void | Function to add files | | addFilesAsync | (files: { blobUrl: string; name: string; userProvidedId: string }[]) => Promise<FolioFile[]> | Async version returning a promise | | isAdding | boolean | Whether files are being added | | isError | boolean | Whether an error occurred | | error | Error \| null | Error object if an error occurred | | newFiles | FolioFile[] \| undefined | The newly added files if available |

useAddFolioDirectoriesWithFiles(projectId?: number)

Hook for adding one or more directories with files to a project. Directory names must be unique at the root level (duplicates will be silently skipped with a console warning).

| Return Property | Type | Description | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | addDirectoriesWithFiles | (params: DirectoryEntry \| DirectoryEntry[]) => void | Function to add one or more directories with files | | addDirectoriesWithFilesAsync | (params: DirectoryEntry \| DirectoryEntry[]) => Promise<{ directory: FolioFile \| null; files: FolioFile[] } \| Array<{ directory: FolioFile \| null; files: FolioFile[] }>> | Async version returning a promise. Returns a single result when given a single directory, or an array of results when given multiple directories | | isAdding | boolean | Whether the directories and files are being added | | isError | boolean | Whether an error occurred | | error | Error \| null | Error object if an error occurred | | result | { directory: FolioFile \| null; files: FolioFile[] } \| Array<{ directory: FolioFile \| null; files: FolioFile[] }> \| undefined | The newly added directories and files. If a directory is null in a result, it means a directory with that name already existed |

useFolioUserMetadata()

Hook for retrieving and updating the current user's metadata.

| Return Property | Type | Description | | --------------------- | ------------------------------------------------------------ | ---------------------------------------- | | metadata | string \| null | The user's metadata as a string, or null | | isLoading | boolean | Whether metadata is being loaded | | isError | boolean | Whether an error occurred | | error | Error \| null | Error object if an error occurred | | updateMetadata | (metadata: Record<string, MetadataValue>) => void | Function to update user metadata | | updateMetadataAsync | (metadata: Record<string, MetadataValue>) => Promise<void> | Async version returning a promise | | isUpdating | boolean | Whether metadata is being updated | | refetch | () => Promise<...> | Function to manually refetch metadata |

Types

The library exports these TypeScript types:

| Type | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | FolioFile | Represents a file in Folio. Contains properties: id, name, blobUrl, parentId (null for root items), isDirectory (boolean), userProvidedId (string), createdAt (Date), and updatedAt (Date) | | FolioProject | Represents a project in Folio. Contains properties: id, name, createdAt (Date), and updatedAt (Date) | | MetadataValue | Represents metadata values that can be nested. Can be a string, number, boolean, null, object, or array of these types. Used for both directory metadata and user metadata. | | DirectoryEntry | Represents a directory with metadata and files to be added to Folio. Contains properties: directoryName, directoryMetadata (now supports nested objects), and files | | FolioFileInput | Input type for adding files to Folio. Contains properties: blobUrl, name, and userProvidedId (required for deduplication) | | AnalyticsEvent | A union type for analytics events sent by Folio. Each event has a name property (like "page_view" or "file_view") and a data object with event-specific parameters. See the Analytics Event Structure section for details on all event types. | | DeepLinkEvent | Event emitted when a user clicks a deep link button in Folio. Contains properties: userProvidedId (string) and metadata (Record<string, unknown> | null). See the Deep Linking section for details. | | FolioEmbedProps | Props for the FolioEmbed component | | FolioProviderProps | Props for the FolioProvider component | | FolioClient | Interface for the client that interacts with the Folio API |

License