npm package: @manyrows/appkit-react
Description: React SDK for Manyrows AppKit. Provides authentication, real-time data, and image management for customer-facing apps.
Latest version: 1.1.0

# @manyrows/appkit-react

React SDK for Manyrows AppKit. Provides authentication, real-time data, and image management for customer-facing apps.

## Installation

```bash
npm install @manyrows/appkit-react
```

## Quick Start

```tsx
import { AppKit, AppKitAuthed, useUser, UserButton } from "@manyrows/appkit-react";

function MyApp() {
  const user = useUser();

  return (
    <div>
      <header style={{ display: "flex", justifyContent: "space-between", padding: 16 }}>
        <h1>My App</h1>
        <UserButton />
      </header>
      {user && <p>Welcome, {user.name || user.email}</p>}
    </div>
  );
}

export default function Page() {
  return (
    <AppKit workspace="acme" appId="your-app-id">
      <AppKitAuthed fallback={null}>
        <MyApp />
      </AppKitAuthed>
    </AppKit>
  );
}
```

Only `workspace` and `appId` are required. The runtime script loads automatically.

### Convenience Hooks

Access common data without drilling into `snapshot`:

```tsx
import {
  useUser,         // user's account (email, name)
  useRoles,        // string[] of roles
  usePermissions,  // string[] of permissions
  usePermission,   // check a single permission: usePermission("edit")
  useRole,         // check a single role: useRole("admin")
  useFeatureFlags, // all feature flags
  useFeatureFlag,  // check one flag: useFeatureFlag("dark-mode")
  useConfig,       // all config values
  useConfigValue,  // single config: useConfigValue("api-url", "default")
  useToken,        // JWT token for API calls
  useAuthFetch,    // fetch wrapper with automatic Bearer token
} from "@manyrows/appkit-react";
```

### Auth Patterns

**Option A (recommended):** Use `<AppKitAuthed>` to gate authenticated content:

```tsx
<AppKit workspace="acme" appId="your-app-id">
  <AppKitAuthed fallback={null}>
    <MyApp />
  </AppKitAuthed>
</AppKit>
```

**Option B:** Gate manually with `useAppKit()`:

```tsx
<AppKit workspace="acme" appId="your-app-id">
  {useAppKit().isAuthenticated ? <MyApp /> : null}
</AppKit>
```

---

## Images

The SDK includes a full image management system — upload, browse, display, edit, and delete — backed by the `manyrows-images` service.

### Setup

Images work automatically when the workspace has an `imagesBaseURL` configured. All image hooks and components read auth and config from the AppKit context, so they must be rendered inside `<AppKitAuthed>`.

### useImages

Hook for listing, searching, editing, and deleting images.

```tsx
import { useImages } from "@manyrows/appkit-react";

function Gallery() {
  const {
    images,       // ImageResource[]
    loading,
    error,
    page,
    pageCount,
    total,
    setPage,
    setQuery,     // search by title
    refetch,
    removeImage,  // (imageId) => Promise<void>
    updateImage,  // (imageId, { title, description }) => Promise<void>
    available,    // false if images service not configured
  } = useImages({ page: 0, pageSize: 20 });

  if (!available) return null;
  // ...
}
```

**Options:**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `page` | number | 0 | Starting page (zero-indexed) |
| `pageSize` | number | 50 | Items per page |
| `q` | string | — | Search query |
| `enabled` | boolean | true | Enable/disable fetching |

### useImageUpload

Hook for uploading images with progress tracking.

```tsx
import { useImageUpload } from "@manyrows/appkit-react";

function Uploader() {
  const { upload, cancel, progress, reset, available } = useImageUpload();

  const handleUpload = async (file: File) => {
    await upload({
      file,
      title: "My image",
      description: "Optional description",
      variants: "sq200,w400,w800",
    });
  };

  return (
    <div>
      <p>Status: {progress.status}</p>
      <p>Progress: {progress.progress}%</p>
      <button onClick={cancel}>Cancel</button>
    </div>
  );
}
```

**Progress state:**

| Field | Type | Description |
|-------|------|-------------|
| `status` | `"idle" \| "uploading" \| "success" \| "error"` | Current state |
| `progress` | number | 0–100 percentage |
| `bytesUploaded` | number | Bytes transferred |
| `bytesTotal` | number | Total file size |
| `error` | string \| null | Error message |

### MrImage

Responsive image component with automatic variant selection.

```tsx
import { MrImage } from "@manyrows/appkit-react";

// Auto-selects best variant for dimensions + device DPR
<MrImage image={image} width={400} height={300} />

// Explicit variant
<MrImage image={image} variant="sq200" />

// Full-size original
<MrImage image={image} />
```

**Props:**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `image` | ImageResource | required | Image to display |
| `width` | number | — | Display width (used for variant selection) |
| `height` | number | — | Display height |
| `variant` | string | — | Explicit variant name (disables auto-selection) |
| `alt` | string | image.title | Alt text |
| `className` | string | — | CSS class |
| `style` | CSSProperties | — | Inline styles |
| `loading` | `"lazy" \| "eager"` | `"lazy"` | Browser loading strategy |
| `objectFit` | CSSProperties["objectFit"] | `"cover"` | CSS object-fit |
| `sizes` | string | — | Responsive sizes attribute |
| `onLoad` | () => void | — | Load callback |
| `onError` | () => void | — | Error callback |

Generates a `srcSet` from available width-based variants for responsive loading.

### ImageUploader

Drop-in upload form with drag-and-drop, preview, variant selection, and progress.

```tsx
import { ImageUploader } from "@manyrows/appkit-react";

<ImageUploader
  onUpload={() => console.log("Done!")}
  onError={(err) => console.error(err)}
  defaultTitle="Untitled"
/>
```

**Props:**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `onUpload` | () => void | — | Called on successful upload |
| `onError` | (error: string) => void | — | Called on upload error |
| `defaultTitle` | string | — | Pre-fill title |
| `defaultDescription` | string | `""` | Pre-fill description |
| `variants` | string | `"sq200,w400,w800"` | Variant names to generate |
| `showFields` | boolean | true | Show title/description inputs |
| `showVariantPicker` | boolean | false | Show advanced variant selector |
| `accept` | string | `"image/png,image/jpeg,..."` | Accepted MIME types |
| `maxSize` | number | 4194304 | Max file size in bytes |
| `label` | string | `"Drop an image here or click to select"` | Drop zone text |
| `disabled` | boolean | false | Disable all interactions |
| `className` | string | — | CSS class |
| `style` | CSSProperties | — | Inline styles |

Features:
- Drag-and-drop or click to select
- Image preview before upload
- File size validation
- Progress bar during upload
- Success toast (auto-dismisses after 4s)
- Auto-populates title from filename
- Collapsible "Advanced" section for variant selection

### ImagePicker

Image grid browser with search, pagination, and optional inline actions.

```tsx
import { ImagePicker } from "@manyrows/appkit-react";

// Inline grid
<ImagePicker
  onSelect={(image) => setSelected(image)}
  pageSize={20}
  showSearch
  showActions
/>

// Modal overlay
<ImagePicker
  mode="modal"
  onSelect={handleSelect}
  onClose={() => setOpen(false)}
/>

// Refresh after upload
const [uploadCount, setUploadCount] = useState(0);
<ImageUploader onUpload={() => setUploadCount(c => c + 1)} />
<ImagePicker refreshSignal={uploadCount} />
```

**Props:**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `onSelect` | (image: ImageResource) => void | — | Called when an image is clicked |
| `onClose` | () => void | — | Called when modal is closed |
| `mode` | `"modal" \| "inline"` | `"inline"` | Display mode |
| `pageSize` | number | 20 | Images per page |
| `showSearch` | boolean | true | Show search bar |
| `searchPlaceholder` | string | `"Search images..."` | Placeholder text |
| `selectedImageId` | string | — | Highlight selected image |
| `columns` | number | 4 | Grid columns |
| `showActions` | boolean | false | Show Info/Edit/Delete on hover |
| `refreshSignal` | number | — | Increment to trigger refetch |
| `className` | string | — | CSS class |
| `style` | CSSProperties | — | Inline styles |

### ImageDetails

Full-screen detail modal for a single image. Shows variant selector, metadata, copy-to-clipboard, and action buttons.

```tsx
import { ImageDetails } from "@manyrows/appkit-react";

<ImageDetails
  image={selectedImage}
  onClose={() => setSelected(null)}
  onEdit={(img) => openEditor(img)}
  onDelete={(img) => confirmDelete(img)}
/>
```

**Props:**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `image` | ImageResource | required | Image to display |
| `onClose` | () => void | required | Close callback |
| `onEdit` | (image: ImageResource) => void | — | Edit button callback |
| `onDelete` | (image: ImageResource) => void | — | Delete button callback |
| `className` | string | — | CSS class |
| `style` | CSSProperties | — | Inline styles |

Displays:
- Large image preview with variant selector tabs
- File size, dimensions, format
- Image ID and URL (both copyable)
- Upload date and description

---

## Types

```typescript
interface ImageResource {
  imageId: string;
  title: string;
  description: string;
  originalName: string;
  uploadedAt: string;
  uploadedBy?: string;
  visibility: string;    // "private" | "shared"
  variants: ImageVariant[];
}

interface ImageVariant {
  id: string;
  variant: string;       // "sq200", "w800", "orig", etc.
  s3Key: string;
  content_type: string;
  format: string;
  size_bytes: number;
  sha256_hex: string;
  ext: string;
  width: number;
  height: number;
  etag: string;
  versionId: string;
  objectURL: string;     // presigned CloudFront URL
  expiresAt: string;
}

interface ImageListResponse {
  images: ImageResource[];
  page: number;
  pageSize: number;
  total: number;
  pageCount: number;
}

interface ImageUploadOptions {
  file: File;
  title: string;
  description?: string;
  variants?: string;     // comma-separated, e.g. "sq200,w400,w800"
  visibility?: "private" | "shared";  // default: "private"
}

interface ImageUpdateOptions {
  title: string;
  description?: string;
  visibility?: "private" | "shared";
}

interface UploadProgress {
  status: "idle" | "uploading" | "success" | "error";
  progress: number;
  bytesUploaded: number;
  bytesTotal: number;
  error: string | null;
}
```

## Available Variants

| Name | Description |
|------|-------------|
| `sq200` | Thumbnail (200x200 square crop) |
| `sq400` | Square (400x400) |
| `w400` | Small (400px wide) |
| `w800` | Medium (800px wide) |
| `w1200` | Large (1200px wide) |
| `w1600` | Retina (1600px wide) |
| `w1920` | Full HD (1920px wide) |
| `orig` | Original upload (always created) |