npm package: @plugable-io/react
Description: React components and hooks for Plugable File Management API
Latest version: 0.0.13

# @plugable-io/react

React components and hooks for the Plugable File Management API.

## Installation

```bash
npm install @plugable-io/react
```

## Components & Hooks

| Export | Type | Description |
|--------|------|-------------|
| `PlugableProvider` | Component | Context provider for configuration |
| `Dropzone` | Component | Drag & drop file uploader |
| `FilePreview` | Component | Universal file preview (images + file type icons) |
| `FileImage` | Component | Image display with caching |
| `useFiles` | Hook | File fetching with pagination |
| `usePlugable` | Hook | Access client and context |

## Quick Start

### Provider Setup

Wrap your app with `PlugableProvider`. You can provide a `getToken` function or use a supported auth provider.

#### With Clerk

```tsx
import { PlugableProvider } from '@plugable-io/react';

function App() {
  return (
    <PlugableProvider
      bucketId="your-bucket-id"
      authProvider="clerk"
      clerkJWTTemplate="plugable" // optional
    >
      <YourApp />
    </PlugableProvider>
  );
}
```

#### With Supabase

```tsx
<PlugableProvider
  bucketId="your-bucket-id"
  authProvider="supabase"
>
  <YourApp />
</PlugableProvider>
```

#### With Custom getToken

```tsx
<PlugableProvider
  bucketId="your-bucket-id"
  getToken={async () => {
    return await getAuthToken();
  }}
>
  <YourApp />
</PlugableProvider>
```

### Theme Customization

The library comes with gorgeous default styling inspired by modern design principles. You can easily customize the appearance using theme props:

#### Dark Mode (Default)

```tsx
<PlugableProvider
  bucketId="your-bucket-id"
  authProvider="clerk"
  theme="dark" // or omit - dark is default
>
  <YourApp />
</PlugableProvider>
```

#### Light Mode

```tsx
<PlugableProvider
  bucketId="your-bucket-id"
  authProvider="clerk"
  theme="light"
>
  <YourApp />
</PlugableProvider>
```

#### Auto (System Preference)

```tsx
<PlugableProvider
  bucketId="your-bucket-id"
  authProvider="clerk"
  theme="auto" // Follows user's system preference
>
  <YourApp />
</PlugableProvider>
```

#### Custom Accent Color

```tsx
<PlugableProvider
  bucketId="your-bucket-id"
  authProvider="clerk"
  accentColor="#10b981" // Green accent
>
  <YourApp />
</PlugableProvider>
```

#### Custom Base Color

```tsx
<PlugableProvider
  bucketId="your-bucket-id"
  authProvider="clerk"
  baseColor="#1e293b" // Custom background
  theme="dark"
>
  <YourApp />
</PlugableProvider>
```

#### Complete Custom Theme

```tsx
<PlugableProvider
  bucketId="your-bucket-id"
  authProvider="clerk"
  theme="light"
  accentColor="#0ea5e9" // Cyan accent
  baseColor="#f8fafc" // Light gray background
>
  <YourApp />
</PlugableProvider>
```

### CSS Variables

The library uses CSS variables for theming, which are scoped to prevent conflicts with your app's styles. You can override any variable:

```css
.plugable-root {
  /* Accent colors */
  --plugable-accent-primary: #9333ea;
  --plugable-accent-secondary: #2563eb;
  --plugable-accent-hover: #7c3aed;
  
  /* Base colors */
  --plugable-base-bg: #0f172a;
  --plugable-base-surface: rgba(30, 41, 59, 0.5);
  --plugable-base-border: rgba(255, 255, 255, 0.1);
  
  /* Text colors */
  --plugable-text-primary: #f1f5f9;
  --plugable-text-secondary: #cbd5e1;
  --plugable-text-muted: #64748b;
  
  /* State colors */
  --plugable-success: #34d399;
  --plugable-error: #f87171;
  --plugable-warning: #fbbf24;
  
  /* Effects */
  --plugable-overlay: rgba(0, 0, 0, 0.3);
  --plugable-backdrop-blur: blur(24px);
}
```

---

## Dropzone

File uploader with drag-and-drop support. Works with default UI or custom render function.

### Default UI

```tsx
import { Dropzone } from '@plugable-io/react';

function ProfileUploader() {
  return (
    <Dropzone
      metadata={{ category: 'avatar', userId: '123' }}
      accept="image/*"
      maxFiles={1}
      onUploadComplete={(files) => {
        console.log('Avatar uploaded:', files[0]);
      }}
      onUploadError={(error) => {
        console.error('Upload failed:', error);
      }}
    />
  );
}
```

### Custom UI with Render Props

```tsx
import { Dropzone } from '@plugable-io/react';

function DocumentUploader() {
  return (
    <Dropzone
      metadata={{ type: 'document' }}
      accept=".pdf,.doc,.docx"
      maxFiles={10}
      onUploadComplete={(files) => console.log('Uploaded:', files)}
    >
      {({ isDragActive, isUploading, uploadProgress, openFileDialog, uploadedFiles }) => (
        <div
          onClick={openFileDialog}
          className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer
            ${isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300'}`}
        >
          {isUploading ? (
            <div className="space-y-2">
              <p className="font-medium">Uploading...</p>
              {Object.entries(uploadProgress).map(([name, progress]) => (
                <div key={name} className="text-sm">
                  <span>{name}</span>
                  <div className="w-full bg-gray-200 rounded h-2 mt-1">
                    <div
                      className="bg-blue-500 h-2 rounded transition-all"
                      style={{ width: `${progress}%` }}
                    />
                  </div>
                </div>
              ))}
            </div>
          ) : (
            <>
              <p className="font-medium">
                {isDragActive ? 'Drop files here' : 'Drag & drop documents'}
              </p>
              <p className="text-sm text-gray-500 mt-1">or click to browse</p>
            </>
          )}

          {uploadedFiles.length > 0 && (
            <div className="mt-4 text-sm text-green-600">
              ✓ {uploadedFiles.length} file(s) uploaded
            </div>
          )}
        </div>
      )}
    </Dropzone>
  );
}
```

### Props

| Prop | Type | Description |
|------|------|-------------|
| `bucketId` | `string` | Override default bucket ID |
| `metadata` | `Record<string, any>` | Metadata to attach to uploaded files |
| `accept` | `string` | File types to accept (e.g., `"image/*"`, `".pdf,.doc"`) |
| `maxFiles` | `number` | Maximum number of files |
| `onUploadComplete` | `(files: FileObject[]) => void` | Called when uploads complete |
| `onUploadError` | `(error: Error) => void` | Called on upload error |
| `onProgressUpdate` | `(fileName: string, progress: number) => void` | Progress callback |
| `children` | `(props: DropzoneRenderProps) => ReactNode` | Custom render function |
| `className` | `string` | CSS class name |
| `style` | `CSSProperties` | Inline styles |

---

## useFiles Hook

Fetch and paginate files with automatic refresh on uploads.

### Basic Usage

```tsx
import { useFiles, FilePreview } from '@plugable-io/react';

function InvoiceList() {
  const { files, isLoading, pagination, refresh } = useFiles({
    metadata: { type: 'invoice' },
    perPage: 10,
  });

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

  return (
    <div>
      <div className="grid grid-cols-4 gap-4">
        {files.map((file) => (
          <div key={file.id} className="border rounded p-3">
            <FilePreview file={file} width={60} height={60} />
            <p className="text-sm mt-2 truncate">{file.name}</p>
            <div className="flex gap-2 mt-2">
              <button onClick={() => file.delete()}>Delete</button>
              <a href={file.download_url} download>Download</a>
            </div>
          </div>
        ))}
      </div>

      <div className="flex gap-2 mt-4">
        <button
          onClick={pagination.loadPreviousPage}
          disabled={!pagination.hasPrevious}
        >
          Previous
        </button>
        <span>Page {pagination.current}</span>
        <button
          onClick={pagination.loadNextPage}
          disabled={!pagination.hasNext}
        >
          Next
        </button>
      </div>
    </div>
  );
}
```

### Filter by Media Type

```tsx
function ImageGallery() {
  const { files, isLoading } = useFiles({
    mediaType: 'image',
    perPage: 20,
  });

  return (
    <div className="grid grid-cols-3 gap-4">
      {files.map((file) => (
        <FilePreview key={file.id} file={file} width="100%" height={200} objectFit="cover" />
      ))}
    </div>
  );
}
```

### Ordering Files

```tsx
function SortedFileList() {
  const [orderBy, setOrderBy] = useState<'created_at' | 'name' | 'byte_size'>('created_at');
  const [orderDirection, setOrderDirection] = useState<'asc' | 'desc'>('desc');

  const { files, isLoading } = useFiles({
    orderBy,
    orderDirection,
    perPage: 20,
  });

  return (
    <div>
      <select value={orderBy} onChange={(e) => setOrderBy(e.target.value as any)}>
        <option value="created_at">Date</option>
        <option value="name">Name</option>
        <option value="byte_size">Size</option>
      </select>
      <button onClick={() => setOrderDirection(orderDirection === 'asc' ? 'desc' : 'asc')}>
        {orderDirection === 'asc' ? '↑' : '↓'}
      </button>
      {files.map((file) => (
        <div key={file.id}>{file.name}</div>
      ))}
    </div>
  );
}
```

### Manual Loading

```tsx
function LazyFileList() {
  const { files, isLoading, refresh } = useFiles({
    metadata: { folder: 'reports' },
    autoLoad: false, // Don't load on mount
  });

  return (
    <div>
      <button onClick={refresh}>Load Files</button>
      {isLoading && <span>Loading...</span>}
      {files.map((file) => (
        <div key={file.id}>{file.name}</div>
      ))}
    </div>
  );
}
```

### Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `metadata` | `Record<string, any>` | - | Filter files by metadata |
| `mediaType` | `string` | - | Filter by media type (`image`, `video`, etc.) |
| `perPage` | `number` | `20` | Files per page |
| `startPage` | `number` | `1` | Initial page number |
| `autoLoad` | `boolean` | `true` | Fetch on mount |
| `orderBy` | `'created_at' \| 'name' \| 'byte_size'` | `'created_at'` | Field to order by |
| `orderDirection` | `'asc' \| 'desc'` | `'desc'` | Sort direction |

### Return Value

| Property | Type | Description |
|----------|------|-------------|
| `files` | `FileObject[]` | Array of files |
| `isLoading` | `boolean` | Loading state |
| `pagination.current` | `number` | Current page |
| `pagination.hasNext` | `boolean` | Has next page |
| `pagination.hasPrevious` | `boolean` | Has previous page |
| `pagination.loadNextPage` | `() => void` | Go to next page |
| `pagination.loadPreviousPage` | `() => void` | Go to previous page |
| `setPage` | `(page: number) => void` | Jump to specific page |
| `refresh` | `() => Promise<void>` | Refresh current page |

---

## FilePreview

Universal file preview component. Displays images inline and shows file type icons for other files.

### Basic Usage

```tsx
import { FilePreview } from '@plugable-io/react';

function FileCard({ file }) {
  return (
    <div className="flex items-center gap-3 p-3 border rounded">
      <FilePreview file={file} width={48} height={48} />
      <div>
        <p className="font-medium">{file.name}</p>
        <p className="text-sm text-gray-500">{file.content_type}</p>
      </div>
    </div>
  );
}
```

### Custom Non-Image Rendering

```tsx
import { FilePreview } from '@plugable-io/react';

function CustomFilePreview({ file }) {
  return (
    <FilePreview
      file={file}
      width={80}
      height={80}
      objectFit="cover"
      renderNonImage={(file) => (
        <div className="flex flex-col items-center justify-center text-gray-500">
          <span className="text-2xl">📄</span>
          <span className="text-xs mt-1">{file.name.split('.').pop()}</span>
        </div>
      )}
    />
  );
}
```

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `file` | `FileObject` | required | File to preview |
| `width` | `number \| string` | `80` | Preview width |
| `height` | `number \| string` | `80` | Preview height |
| `objectFit` | `'contain' \| 'cover' \| 'fill' \| 'none' \| 'scale-down'` | `'cover'` | Image fit mode |
| `className` | `string` | - | CSS class name |
| `style` | `CSSProperties` | - | Inline styles |
| `showExtension` | `boolean` | `true` | Show file extension for non-images |
| `renderNonImage` | `(file: FileObject) => ReactNode` | - | Custom renderer for non-image files |

---

## FileImage

Display images with automatic URL fetching and caching.

```tsx
import { FileImage } from '@plugable-io/react';

function Avatar({ file }) {
  return (
    <FileImage
      file={file}
      width={100}
      height={100}
      objectFit="cover"
      borderRadius={50}
      alt="User avatar"
      onLoad={() => console.log('Loaded')}
    />
  );
}
```

---

## File Object Methods

Each `FileObject` provides built-in methods for updating, downloading, and deleting files:

```tsx
// Update file metadata or name
await file.update(
  { tags: ['important', 'reviewed'] }, // metadata (optional)
  'new-filename.txt'                   // name (optional)
);

// Download the file (initiates browser download)
await file.download();

// Delete the file
await file.delete();

// Access properties
console.log(file.id);           // Unique ID
console.log(file.name);         // Filename
console.log(file.content_type); // MIME type
console.log(file.download_url); // Signed download URL
console.log(file.metadata);     // Custom metadata
```

---

## Full Example

Complete file manager with upload, list, and preview:

```tsx
import { 
  PlugableProvider, 
  Dropzone, 
  useFiles, 
  FilePreview 
} from '@plugable-io/react';

function FileManager() {
  const { files, isLoading, pagination, refresh } = useFiles({
    perPage: 12,
  });

  return (
    <div className="max-w-4xl mx-auto p-6">
      <h1 className="text-2xl font-bold mb-6">File Manager</h1>

      {/* Upload Section */}
      <Dropzone
        maxFiles={5}
        onUploadComplete={(uploaded) => {
          console.log('Uploaded:', uploaded);
          // Files list auto-refreshes via event system
        }}
        className="mb-8"
      />

      {/* Files Grid */}
      {isLoading ? (
        <div className="text-center py-8">Loading...</div>
      ) : (
        <div className="grid grid-cols-4 gap-4">
          {files.map((file) => (
            <div key={file.id} className="border rounded-lg p-3">
              <FilePreview 
                file={file} 
                width="100%" 
                height={120} 
                objectFit="cover" 
              />
              <p className="text-sm mt-2 truncate" title={file.name}>
                {file.name}
              </p>
              <div className="flex gap-2 mt-2">
                <a 
                  href={file.download_url} 
                  download
                  className="text-blue-500 text-sm"
                >
                  Download
                </a>
                <button 
                  onClick={() => file.delete()}
                  className="text-red-500 text-sm"
                >
                  Delete
                </button>
              </div>
            </div>
          ))}
        </div>
      )}

      {/* Pagination */}
      <div className="flex justify-center gap-4 mt-6">
        <button
          onClick={pagination.loadPreviousPage}
          disabled={!pagination.hasPrevious}
          className="px-4 py-2 border rounded disabled:opacity-50"
        >
          Previous
        </button>
        <span className="py-2">Page {pagination.current}</span>
        <button
          onClick={pagination.loadNextPage}
          disabled={!pagination.hasNext}
          className="px-4 py-2 border rounded disabled:opacity-50"
        >
          Next
        </button>
      </div>
    </div>
  );
}

// App wrapper
function App() {
  return (
    <PlugableProvider
      bucketId="your-bucket-id"
      authProvider="clerk"
    >
      <FileManager />
    </PlugableProvider>
  );
}
```

---

## TypeScript

All types are exported:

```tsx
import type {
  PlugableProviderProps,
  AuthProvider,
  DropzoneProps,
  DropzoneRenderProps,
  FilePreviewProps,
  FileImageProps,
  FileListProps,
  FileListRenderProps,
  UseFilesOptions,
  UseFilesResult,
  FileObject,
  SearchOptions,
  UpdateOptions,
} from '@plugable-io/react';
```

## License

MIT