npm package: @potion5/react
Description: Embeddable React components for the Potion API - AI-powered beverage formulation platform
Author: Potion
Latest version: 1.0.0

# @potion5/react

Embeddable React components for the Potion API. Build beverage formulation features into your application with pre-built, customizable components.

## Installation

```bash
npm install @potion5/react @potion5/sdk
# or
yarn add @potion5/react @potion5/sdk
# or
pnpm add @potion5/react @potion5/sdk
```

## Quick Start

### React Applications

```tsx
import { PotionProvider, FormulationBuilder } from '@potion5/react';
import '@potion5/react/styles';

function App() {
  return (
    <PotionProvider apiKey="pk_live_your_api_key">
      <FormulationBuilder
        enableAI={true}
        onComplete={(formulation) => {
          console.log('Created formulation:', formulation);
        }}
      />
    </PotionProvider>
  );
}
```

### Vanilla JavaScript (Non-React Apps)

```html
<script src="https://cdn.potion.com/embed.js"></script>
<div id="potion-container"></div>
<script>
  PotionEmbed.create({
    apiKey: 'pk_live_your_api_key',
    container: '#potion-container',
    component: 'FormulationBuilder',
    props: {
      enableAI: true,
      onComplete: function(formulation) {
        console.log('Created:', formulation);
      }
    },
    theme: {
      mode: 'light',
      primaryColor: '#6366f1'
    }
  });
</script>
```

## Components

### FormulationBuilder

Create and edit beverage formulations with AI assistance.

```tsx
<FormulationBuilder
  mode="create" // 'create' | 'edit'
  formulationId="..." // Required for edit mode
  category="energy-drink"
  enableAI={true}
  onComplete={(formulation) => void}
  onCancel={() => void}
  onChange={(formulation) => void}
  showProperties={true}
/>
```

### IngredientSearch

Smart ingredient finder with allergen warnings and GRAS status.

```tsx
<IngredientSearch
  onSelect={(ingredient) => void}
  grasOnly={false}
  excludeAllergens={['peanuts', 'milk']}
  maxResults={20}
  showDetails={true}
  placeholder="Search ingredients..."
  compact={false}
/>
```

### ComplianceChecker

Comprehensive compliance checking with checklist generation.

```tsx
<ComplianceChecker
  formulationId="..."
  generateChecklist={true}
  generateLabelingRequirements={true}
  includeRegulatoryCitations={true}
  jurisdictions={['CA', 'NY']}
  onChecklistGenerated={(items) => void}
  onLabelingGenerated={(requirements) => void}
  onExport={(format, data) => void}
/>
```

### SOPEditor

Full SOP lifecycle management - create, view, and edit.

```tsx
<SOPEditor
  mode="create" // 'create' | 'view' | 'edit'
  formulationId="..."
  sopId="..." // Required for view/edit
  scale="pilot" // 'lab' | 'pilot' | 'commercial'
  generateWithAI={true}
  showCriticalControlPoints={true}
  onSave={(sop) => void}
  onPublish={(sop) => void}
  onStepComplete={(stepNumber) => void}
  onExport={(format, blob) => void}
/>
```

### PotionAssistant

Unified AI chat interface for formulation assistance.

```tsx
<PotionAssistant
  formulationId="..." // Optional context
  position="inline" // 'bottom-right' | 'bottom-left' | 'inline' | 'fullscreen'
  defaultExpanded={true}
  placeholder="Ask anything..."
  welcomeMessage="Hi! I'm your formulation assistant."
  suggestedPrompts={[
    "What's the shelf life?",
    "Check compliance issues"
  ]}
  enableVisualization={true}
  enableActions={true}
  onAction={(action, data) => void}
  height="600px"
/>
```

## Theming

### CSS Variables

Override CSS custom properties to match your brand:

```css
:root {
  --potion-color-primary: #your-brand-color;
  --potion-color-accent: #your-accent-color;
  --potion-font-family: 'Your Font', sans-serif;
  --potion-radius-default: 8px;
}
```

### Theme Provider

```tsx
<PotionProvider
  apiKey="pk_live_xxx"
  theme={{
    mode: 'light', // 'light' | 'dark' | 'system'
    primaryColor: '#6366f1',
    accentColor: '#8b5cf6',
    borderRadius: 'md', // 'none' | 'sm' | 'md' | 'lg' | 'full'
    fontFamily: 'Inter, sans-serif',
  }}
>
  {/* Your components */}
</PotionProvider>
```

### Dynamic Theme Updates

```tsx
function ThemeSwitcher() {
  const { theme, setTheme } = usePotionTheme();

  return (
    <button onClick={() => setTheme({ mode: 'dark' })}>
      Toggle Dark Mode
    </button>
  );
}
```

## Hooks

### useFormulation

```tsx
const { formulation, isLoading, error, refetch, update, remove } = useFormulation({
  id: 'formulation-id',
  autoFetch: true,
});
```

### useFormulationGenerate

```tsx
const { generate, isGenerating, error, generatedFormulation } = useFormulationGenerate();

await generate({
  category: 'energy-drink',
  description: 'A refreshing citrus energy drink',
  constraints: {
    max_ingredients: 10,
    exclude_allergens: ['milk'],
  },
});
```

### useIngredientSearch

```tsx
const { query, setQuery, results, isSearching, clear } = useIngredientSearch({
  debounce: 300,
  limit: 20,
  grasOnly: true,
});
```

### useComplianceCheck

```tsx
const { result, isChecking, error, check } = useComplianceCheck({
  formulationId: 'xxx',
  autoCheck: false,
});
```

### useComplianceChecklist

```tsx
const { checklist, isGenerating, generate, updateItem, exportChecklist } = useComplianceChecklist({
  formulationId: 'xxx',
  includeFederal: true,
  states: ['CA', 'NY'],
});
```

### useLabelingRequirements

```tsx
const { requirements, isLoading, isGenerating, fetch, generate } = useLabelingRequirements({
  formulationId: 'xxx',
  autoFetch: false,
});
```

## Vanilla JS Embed API

### Create Instance

```javascript
const instance = PotionEmbed.create({
  apiKey: 'pk_live_xxx',
  container: '#my-container',
  component: 'FormulationBuilder',
  props: { enableAI: true },
  theme: { mode: 'dark' },
  onReady: () => console.log('Ready!'),
  onError: (error) => console.error(error),
});
```

### Update Props

```javascript
instance.updateProps({
  formulationId: 'new-id',
  mode: 'edit',
});
```

### Destroy Instance

```javascript
instance.destroy();
// or
PotionEmbed.destroy('#my-container');
// or destroy all
PotionEmbed.destroyAll();
```

### Available Components

```javascript
console.log(PotionEmbed.components);
// ['FormulationBuilder', 'IngredientSearch', 'ComplianceChecker', 'SOPEditor', 'PotionAssistant']
```

## Error Handling

### PotionErrorBoundary

Wrap components with error boundary for production-ready error handling:

```tsx
import { PotionErrorBoundary } from '@potion5/react';

<PotionErrorBoundary
  fallback={<CustomErrorUI />}
  onError={(error, errorInfo) => {
    // Log to error reporting service
    logError(error, errorInfo);
  }}
  showDetails={process.env.NODE_ENV === 'development'}
  reportErrors={true}
  reportEndpoint="/api/errors"
>
  <FormulationBuilder />
</PotionErrorBoundary>
```

### Custom Fallback with Error Details

```tsx
<PotionErrorBoundary
  fallback={(details) => (
    <div>
      <h3>Error: {details.error.message}</h3>
      <p>Component: {details.componentName}</p>
      <p>Time: {details.timestamp}</p>
      {details.errorInfo?.componentStack && (
        <pre>{details.errorInfo.componentStack}</pre>
      )}
    </div>
  )}
  onRetry={() => console.log('Retrying...')}
>
  <FormulationBuilder />
</PotionErrorBoundary>
```

Note: The default fallback UI includes "Try Again" and "Reset" buttons automatically.
For custom fallbacks, use the `onRetry` and `onReset` props on the error boundary.

## Testing

### MockPotionProvider

Test your integrations without hitting real APIs:

```tsx
import { MockPotionProvider, mockFormulation } from '@potion5/react/testing';
import { render, screen } from '@testing-library/react';

test('renders formulation builder', async () => {
  render(
    <MockPotionProvider
      mockData={{
        formulations: [mockFormulation({ name: 'Test Drink' })],
      }}
      config={{
        delay: 0, // No delay for tests
        debug: false,
      }}
    >
      <FormulationBuilder />
    </MockPotionProvider>
  );

  expect(await screen.findByText('Create Formulation')).toBeInTheDocument();
});
```

### Mock Factories

Create test data with customizable factories:

```tsx
import {
  mockFormulation,
  mockIngredient,
  mockComplianceCheckResult,
  mockSOPDocument,
  mockAllergenIngredient,
} from '@potion5/react/testing';

// Basic mock
const formulation = mockFormulation();

// With overrides
const customFormulation = mockFormulation({
  name: 'Custom Energy Drink',
  category: 'energy-drink',
  status: 'approved',
});

// Allergen ingredient
const milkIngredient = mockAllergenIngredient('milk', {
  ingredient_name: 'Milk Protein Isolate',
});
```

### Test Utilities

```tsx
import {
  createPotionWrapper,
  waitForMockDelay,
  createMockHandler,
} from '@potion5/react/testing';

// Create wrapper for multiple tests
const wrapper = createPotionWrapper({
  mockData: { formulations: [] },
});

render(<FormulationBuilder />, { wrapper });

// Wait for async operations
await waitForMockDelay(100);

// Track callbacks
const onComplete = createMockHandler<Formulation>();
expect(onComplete.calls).toHaveLength(1);
```

### Simulating Errors

```tsx
<MockPotionProvider
  config={{
    errors: {
      formulations: new Error('Network error'),
      compliance: 'Service unavailable',
    },
  }}
>
  <FormulationBuilder />
</MockPotionProvider>
```

## Iframe Embedding

For non-technical teams or complete isolation, embed via iframe:

### Basic Iframe

```html
<iframe
  id="potion-embed"
  src="https://embed.potion.com/iframe?apiKey=pk_live_xxx&component=FormulationBuilder"
  style="width: 100%; height: 600px; border: none;"
></iframe>
```

### With Configuration

```html
<iframe
  src="https://embed.potion.com/iframe?apiKey=pk_live_xxx&component=FormulationBuilder&themeMode=dark&primaryColor=%236366f1"
></iframe>
```

### PostMessage Communication

Listen for events from the iframe:

```javascript
import { PotionIframeClient } from '@potion5/react/iframe';

// Wait for iframe to be ready
const iframe = document.getElementById('potion-embed');
await PotionIframeClient.waitForReady(iframe);

// Listen for events
const unsubscribe = PotionIframeClient.listen((message) => {
  switch (message.type) {
    case 'potion:formulation:complete':
      console.log('Formulation created:', message.payload);
      break;
    case 'potion:error':
      console.error('Error:', message.payload);
      break;
  }
});

// Send commands to iframe
PotionIframeClient.updateConfig(iframe, {
  props: { mode: 'edit', formulationId: 'xxx' },
});

// Clean up
unsubscribe();
```

### URL Parameters

| Parameter | Description | Example |
|-----------|-------------|---------|
| `apiKey` | API key (required) | `pk_live_xxx` |
| `component` | Component name (required) | `FormulationBuilder` |
| `props` | JSON props object | `%7B%22enableAI%22%3Atrue%7D` |
| `themeMode` | Theme mode | `light`, `dark`, `system` |
| `primaryColor` | Hex color | `%236366f1` |
| `debug` | Enable debug mode | `1` |

### Building Iframe URL

```typescript
import { buildIframeUrl } from '@potion5/react/iframe';

const url = buildIframeUrl('https://embed.potion.com/iframe', {
  apiKey: 'pk_live_xxx',
  component: 'FormulationBuilder',
  props: { enableAI: true },
  theme: { mode: 'dark', primaryColor: '#6366f1' },
});
```

## TypeScript Support

All components and hooks are fully typed. Import types as needed:

```tsx
import type {
  Formulation,
  Ingredient,
  ComplianceCheckResult,
  SOPDocument,
  FormulationBuilderProps,
  PotionErrorBoundaryProps,
  ErrorDetails,
} from '@potion5/react';

// Testing types
import type {
  MockData,
  MockConfig,
  MockPotionProviderProps,
} from '@potion5/react/testing';

// Iframe types
import type {
  IframeConfig,
  PotionMessage,
  PotionMessageType,
} from '@potion5/react/iframe';
```

## Browser Support

- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)

## License

MIT