# OV25-UI

A modern React UI component library for e-commerce applications, featuring a configurator with 3D product visualization.

## Installation

### For React 19

```bash
npm install ov25-ui
```

### For React 18 (Shopify, WooCommerce, etc.)

```bash
npm install ov25-ui-react18
```

**Note:** Both packages provide identical functionality. Choose the package that matches your React version to avoid dependency conflicts.

## Usage

### ES Modules (recommended)

**For React 19:**
```javascript
import { injectConfigurator } from 'ov25-ui';
```

**For React 18:**
```javascript
import { injectConfigurator } from 'ov25-ui-react18';
```

// Inject the configurator into your page
injectConfigurator({
  apiKey: 'your-api-key',               // Required
  productLink: '/product/123',           // Required
  galleryId: '#product-image-container', // Optional
  carouselId: '#product-carousel',       // Optional
  priceId: '#price-element',              // Optional
  nameId: '#name-element',              // Optional
  variantsId: '#product-variants',        // Optional
  swatchesId: 'ov25-swatches'           // Optional
});
```

### Screenshot Generation

After injecting the configurator, you can generate thumbnails using the `window.ov25GenerateThumbnail()` function:

```javascript
// Generate a screenshot and get the URL
window.ov25GenerateThumbnail()
  .then(screenshotUrl => {
    console.log('Screenshot URL:', screenshotUrl);
    // Use the URL for thumbnails, social sharing, etc.
  })
  .catch(error => {
    console.error('Screenshot generation failed:', error);
  });

// Using async/await
async function captureThumbnail() {
  try {
    const url = await window.ov25GenerateThumbnail();
    return url;
  } catch (error) {
    console.error('Failed to capture thumbnail:', error);
  }
}
```

The function returns a Promise that resolves with the uploaded screenshot URL from S3. Screenshots are automatically uploaded and the returned URL can be used immediately.

## Publishing

This package is published in two versions to support both React 18 and React 19:

- **`ov25-ui`** - For React 19 consumers (default)
- **`ov25-ui-react18`** - For React 18 consumers (Shopify, WooCommerce, etc.)

### Publishing both versions

1. Update the version number in `package.json`
2. Run `npm run publish:all` to build and publish both packages

### Canary releases

For pre-release testing without affecting the `latest` tag:

1. Bump to a prerelease version: `npm version prerelease --preid=canary` (e.g. `0.5.73` → `0.5.74-canary.0`)
2. Run `npm run publish:canary` to build and publish both packages under the `canary` tag

Consumers install with `npm install ov25-ui@canary` or `npm install ov25-ui-react18@canary`. Regular `npm install ov25-ui` continues to resolve to `latest`.

### Publishing individual versions (don't do this usually)

- React 18: `npm run publish:react18`
- React 19: `npm run publish:react19`

### Building without publishing

- React 18: `npm run build:react18`
- React 19: `npm run build:react19`

### Managing Dependencies

When adding new dependencies to `package.json`, they will automatically be used for both React 18 and React 19 builds. The build scripts handle React version differences automatically.

## Commerce payloads (`onChange` / cart callbacks)

SKU and price data from the iframe are **normalized** before callbacks run:

- **Canonical**: `payload.skus.lines` and `payload.price.lines` (one or many rows). Check `payload.skus.mode` (`'single' | 'multi'`).
- **Legacy (single-product only)**: when `mode === 'single'`, `payload.skus.skuString` and `payload.skus.skuMap` remain set as before.
- **Snap2**: `mode === 'multi'` on SKU payload; use `lines` (no top-level `skuString`).
- Optional **`normalizeSkuPayload` / `normalizePricePayload`** are exported if you handle raw `postMessage` outside the package.

See [CHANGELOG.md](./CHANGELOG.md) for version notes.

## String replacements

Use `stringReplacements` in `injectConfigurator` to customize built-in UI copy.

```javascript
import {
  injectConfigurator,
  STRING_REPLACEMENT_DEFINITIONS,
} from 'ov25-ui';

injectConfigurator({
  // ...existing config
  stringReplacements: {
    productTitle: [
      { template: 'Range: ${RANGE_NAME} / Product: ${PRODUCT_NAME}' },
    ],
    optionHeader: [
      { trigger: { name: 'OPTION_NAME', value: 'leg' }, template: 'Leg Type' },
      { template: '${OPTION_NAME}' },
    ],
  },
});
```

- `STRING_REPLACEMENT_DEFINITIONS` exposes all supported keys and interpolation vars for setup UIs.
- Rules are evaluated case-insensitively for triggers (`trim().toLowerCase()`).
- Triggered rules are checked first in array order; if none match, the first catch-all rule is used.
- If no rule resolves, the component fallback string is shown.

## Testing locally

- There is a minimal react project in dev/react-test which build a simple webpage. 
- It then calls injectConfigurator with some options. (dev/react-test/src/App.jsx)
- The react-test project will need an initial `npm install` (dont install at root, you must `cd dev/react-test` first. it has a separate `package.json`)
- To run, go to root directory of ov25-ui. run `npm run dev`. This will build the project and then spin up the react project
- Go to http://localhost:3000

### Local OV25 configurator dev

Set `OV25_CONFIGURATOR_DEV=true` when building/running `ov25-ui` to point iframe URLs at the local OV25 app via `http://<current-host>:3000/configurator/...`. This keeps LAN devices such as iOS Safari away from `configurator.localhost`. `USE_LOCAL_DEV=true` still works as a legacy alias.

## Running tests locally

- `npm run test:unit` for unit tests
- `npm run test:e2e` for full end-to-end tests (page navigation network requests, all browser features, SLOW)
- `npm run test:browser` for simpler frontend tests (component testing but with real browser APIs)
