# OMX Component SDK Reference

Companion reference for the Mystique SDK Component Scaffolder. This file covers the SDK API surface: type definitions, React hooks, component composition patterns, field development, build lifecycle, and deployment.

**Loaded by:** `fluent-mystique-scaffold`, `fluent-mystique-builder`, `fluent-frontend-dev`

---

## @types/mystique/ Type Definitions

The `@types/mystique/` directory contains 7 TypeScript declaration files that define the Mystique SDK API surface. These must be copied from the reference SDK or generated from the templates.

| File | Purpose |
|------|---------|
| `index.d.ts` | Common types: `TranslatableString`, `QueryResponse<T>`, `Connection<T>`, `Edge<T>` |
| `manifest.d.ts` | Full manifest v2.0 schema: `MystiqueManifest`, `MystiquePage`, `MystiqueComponentInstance`, `MystiquePlugin`, `MystiqueManifestFragment`, `GQLQuery` |
| `components.d.ts` | Component prop interfaces: `Card`, `CardContent`, `Children`, `DynamicValue`, `Loading`, `List`, `QuantityList`, `DatePicker` |
| `hooks.d.ts` | Hook definitions: `useAuth`, `useEnv`, `useI18n`, `useSettings`, `useQuery`, `getQuery`, `useRest`, `getRest`, `useData`, `useUserActions`, `useUserActionForm`, `getApiDownload`, `getSettings` |
| `services.d.ts` | Service types: `MystiqueThemeColours` (color palette interface) |
| `registry.d.ts` | Registry interfaces: `ComponentRegistry`, `FieldRegistry`, `TemplateRegistry` |
| `frame.d.ts` | Frame utilities: `pushModal`, `pushDrawer`, `pushToast`, `DataModal`, `clearModals`, `clearDrawers` |

These type definitions are the contract between your component code and the Mystique host application. The actual implementations are provided at runtime by the host app via `window.Mystique`.

---

## React Hooks API

The Mystique SDK provides 10 hooks and 4 frame utilities via `mystique/hooks/*` and `mystique/frame/*`. All are provided by the host app at runtime — your bundle does NOT include their implementations.

**Hooks:** useAuth, useEnv, useI18n, useSettings/getSettings, useQuery/getQuery, useData, useRest/getRest, getApiDownload, useUserActions, useUserActionForm.

**Frame utilities:** pushModal, pushDrawer, pushToast, DataModal. See `knowledge/frontend/mystique-advanced-sdk.md` §4-5 for full reference with examples.

### useAuth — User Session & Context

```typescript
import { useAuth } from 'mystique/hooks/useAuth';

const auth = useAuth();

// User info
auth.user.username;          // string
auth.user.firstName;         // string
auth.user.timezone;          // string (e.g., "Australia/Sydney")
auth.user.language;          // string (e.g., "en")
auth.user.roles;             // { role: { name: string } }[]

// Current context
auth.context.current.contextId;    // string — retailer or location ID
auth.context.current.contextType;  // "RETAILER" | "LOCATION"
auth.context.current.details?.ref; // string — entity ref

// Actions
auth.switchContext({ contextType: 'RETAILER', contextId });
auth.logout();

// Role checking — conditional rendering based on user roles
const check = auth.context.roleCheck(['ADMIN', 'MANAGER']);
if (check.hasRole) { /* show admin features */ }
```

**Use case**: Role-based rendering, context-aware queries, user info display, location ref for queries.

### useQuery — GraphQL Data Fetching

```typescript
import { useQuery } from 'mystique/hooks/useQuery';

// Returns [UseQueryState<T>, reexecuteFn]
const [result] = useQuery<ResultType>(queryString, { variables });

// result.fetching — boolean (true while loading)
// result.stale    — boolean (true when data is from a previous query)
// result.data     — T | undefined (typed query result)
// result.error    — CombinedError | undefined
// Second element is reexecute function: () => void
```

**Common destructuring patterns:**
```typescript
// Pattern 1: Access via result object (RECOMMENDED)
const [result] = useQuery<OrderByIdResponse>(query, { orderId });
const { fetching, data, error } = result;

// Pattern 2: Rename for clarity
const [orderResult] = useQuery<OrderByIdResponse>(query, { orderId });
if (orderResult.fetching) return <Loading />;
if (orderResult.error) return <ErrorDisplay error={orderResult.error} />;
const order = orderResult.data?.orderById;
```

Results follow the Relay connection pattern: `edges[].node`, `edges[].cursor`, `pageInfo { hasNextPage }`. Cursor lives on each edge, not on pageInfo.

**Prefer page query data over useQuery** — only use useQuery for follow-up queries not available in the page data context. Page queries are defined in the manifest `data` block and auto-executed.

**3rd parameter — conditional execution:**
```typescript
// useQuery accepts a 3rd boolean parameter to conditionally execute the query
const shouldFetch = !!entityId && entityId !== '0';
const [result] = useQuery<ResultType>(query, { id: entityId }, shouldFetch);
// When false: query does NOT execute, result.data stays undefined
// When true: query executes normally
// Reference: Analytics plugin LookerVisualisation uses this for conditional iframe data fetch
```

**getQuery — async GraphQL outside React hooks:**
```typescript
import { getQuery } from 'mystique/hooks/useQuery';

// getQuery is NOT a hook — use it in event handlers, callbacks, or async functions
const handleScan = async (barcode: string) => {
  const result = await getQuery(VALIDATE_BARCODE_QUERY, { barcode });
  if (result.data?.article) {
    dispatch({ type: 'BARCODE_VALID', article: result.data.article });
  }
};
// Reference: Store plugin ScanToStorageArea uses getQuery to validate scanned barcodes
```

**Polling with reexecute:**
```typescript
// Use setTimeout + reexecute for polling backend state changes
const [result, reexecute] = useQuery<StatusResult>(STATUS_QUERY, { id });

useEffect(() => {
  if (!result.data) return;
  const isReady = result.data.entity.status === 'READY';
  // Poll fast while processing, slow when stable
  const timer = setTimeout(reexecute, isReady ? 10000 : 1000);
  return () => clearTimeout(timer);
}, [result.data]);
// Reference: Store plugin MultiParcelField polls for label generation status
```

**Smart polling with adaptive intervals (OOTB pattern):**

OOTB components use adaptive polling intervals — fast while waiting for data, slower once stable:

```typescript
const FAST_INTERVAL = 1000;  // 1s while waiting
const SLOW_INTERVAL = 10000; // 10s once data ready

const [result, reexecute] = useQuery(QUERY, variables);

useEffect(() => {
  const interval = dataReady ? SLOW_INTERVAL : FAST_INTERVAL;
  const timer = setInterval(() => reexecute(), interval);
  return () => clearInterval(timer);
}, [dataReady]);
```

Use when: Component displays data that changes asynchronously (parcel tracking, fulfilment status, batch processing results). Prefer `setTimeout` + `reexecute` (shown above) for simple cases; use `setInterval` with adaptive rate for long-lived polling scenarios.

### useSettings — Hierarchical Settings Access

```typescript
import { useSettings } from 'mystique/hooks/useSettings';

const settings = useSettings({ key: 'my.setting.name' });

// Result shape — ALWAYS check status before accessing result:
// settings.key.status  — 'loading' | 'ok' | 'error'
// settings.key.result?.value — the setting value (only when status === 'ok')

// Defensive access pattern (REQUIRED):
if (settings.key.status === 'ok') {
  const config = settings.key.result?.value;
} else if (settings.key.status === 'error') {
  // Show error fallback
} else {
  // status === 'loading' — show spinner
}
```

**Hierarchy**: Location > Retailer > Account > Global (cascades up, most specific wins).

**Settings as runtime configuration — cascading defaults (OOTB pattern):**

OOTB components merge fetched settings over hardcoded defaults, ensuring the component always has valid configuration. This guarantees: (1) component works without any settings deployed, (2) new config keys added in code are available immediately, (3) deployed settings override only what is explicitly set.

```typescript
import merge from 'lodash.merge';

const DEFAULTS = {
  columns: ['ref', 'status', 'createdOn'],
  pageSize: 25,
  scannerEnabled: true,
};

const MyComponent = () => {
  const settings = useSettings({ key: 'my.component.config' });
  const config = settings.key.status === 'ok'
    ? merge({}, DEFAULTS, settings.key.result?.value)
    : DEFAULTS;
  return <DataTable columns={config.columns} pageSize={config.pageSize} />;
};
// Reference: Store plugin uses settings for fulfilment types, scanner config, table columns
```

**Always use `merge({}, DEFAULTS, settings)` — never `merge(DEFAULTS, settings)`.** The first argument `{}` prevents mutation of the DEFAULTS constant. `lodash.merge` (deep merge) is preferred over `Object.assign` (shallow) when settings contain nested objects like column configs.

**`getSettings` — async settings outside React hooks:**
```typescript
import { getSettings } from 'mystique/hooks/useSettings';

// NOT a hook — use in event handlers, async functions, startup logic
const loadConfig = async () => {
  const result = await getSettings({ key: 'my.setting.name' });
  return result.key?.result?.value;
};
```

### useI18n — Internationalization

```typescript
import { useI18n } from 'mystique/hooks/useI18n';

const { translate, translateOr, prefixedTranslate, language } = useI18n();

// Basic lookup
translate('fc.om.orders.title');

// translateOr — array with fallback (RECOMMENDED for all user-facing strings)
translateOr(['acme.orders.title', 'Orders']);   // last item = default if key missing

// With interpolation variables
translateOr(
  ['fc.sf.ui.showing', 'Showing {{currentCount}} of {{totalCount}}'],
  { currentCount: 10, totalCount: 50 }
);

// prefixedTranslate — for manifest-configured labels via TemplateRegistry
import { TemplateRegistry } from 'mystique/registry/TemplateRegistry';
prefixedTranslate(TemplateRegistry.render(attribute.label));

// Language info
language.current;    // 'en'
language.available;  // [{ label: 'English', code: 'en' }, ...]
```

**Production rule:** Every user-facing string should use `translateOr` with a fallback. `translate()` returns the key as-is if missing — `translateOr` with a fallback ensures users never see raw i18n keys.

#### translate vs translateOr vs prefixedTranslate — Decision Guide

| Method | When to use | Behaviour when key missing |
|--------|------------|---------------------------|
| `translate(key)` | **Rare** — only when key is guaranteed to exist (e.g., platform keys in core components) | Returns the raw key string — users see `"fc.om.orders.title"` |
| `translateOr([key, fallback])` | **Default for all custom code** — static labels, messages, toasts, tooltips | Returns the last array element as fallback — users see `"Orders"` |
| `prefixedTranslate(value)` | **Manifest-driven labels only** — when the string comes from manifest JSON with `i18n:` prefix | Translates if `i18n:` prefix present, returns value as-is otherwise |

**Rule of thumb:** If you're writing a string literal in code → `translateOr`. If the string comes from manifest/settings config → `prefixedTranslate`. Never use bare `translate()` in custom components.

#### i18n Key Naming Convention

**Pattern:** `{namespace}.{app}.{area}.{component}.{element}`

| Segment | Values | Example |
|---------|--------|---------|
| namespace | `fc` (OOTB), company prefix for custom (e.g., `acme`) | `fc` / `acme` |
| app | `om` (OMS App), `sf` (Store App), `sp` (Store App, legacy alias), custom app code | `sf` |
| area | Feature area: `ui`, `pack`, `wave`, `returns`, `orders` | `ui.pack` |
| component | Component or page name | `index` |
| element | UI element: `title`, `label`, `button`, `placeholder`, `message`, `tooltip`, `column`, `error` | `title` |

**Examples:**
- OOTB: `fc.sf.ui.pack.index.title` → "Pack"
- OOTB: `fc.sf.ui.wave.createWave.list.footer.showing` → "Showing {{currentCount}} of {{totalCount}}"
- Custom: `acme.om.appeasement.form.amount.label` → "Amount"
- Custom: `acme.om.appeasement.toast.success` → "Appeasement created"

**Rules:**
- Dot-separated, lowercase, no spaces or special characters
- Interpolation uses `{{variableName}}` — Handlebars syntax inside translation values
- Keep fallback English text in the `translateOr` call AND in `translation.json` — both must match

#### When to Use i18n

- **Always** for user-visible strings: labels, titles, button text, toast messages, tooltips, table headers, dialog content, empty-state messages
- **Never** for developer strings: `console.log`, error codes, `data-testid`, CSS classNames, GraphQL field names, prop keys
- **Internal admin tools (English-only):** Still use `translateOr` with English fallbacks — makes future l10n possible with zero refactoring

#### Manifest i18n: Prefix Contract

Manifest `title`, `subTitle`, `label`, and `menuLabel` fields MUST use the `i18n:` prefix OR `{{` template syntax. The Mystique framework resolves `i18n:fc.om.orders.title` by looking up `fc.om.orders.title` in the loaded translation file.

```typescript
// In manifest definition (TypeScript):
nav: {
  label: 'i18n:fc.sf.ui.example',   // ← i18n: prefix — resolved by framework
  icon: 'flight_land',
}

// In translation.json:
{ "fc.sf.ui.example": "Example Page" }
```

**Build-time validation (SDK Manifest.test.ts pattern):**

The SDK ships a manifest test (`manifests/Manifest.test.ts`) that enforces TWO rules:
1. `title`, `subTitle`, `label`, `menuLabel` must start with `i18n:` or `{{` — catches hardcoded English
2. Every `i18n:key` must exist in `i18n/locales/en/translation.json` — catches orphaned keys

```typescript
// From SDK Manifest.test.ts — add to your project's manifest tests:
import englishTranslations from '../i18n/locales/en/translation.json';

const internationalizationAssertion = (obj: any, property: string) => {
  if (['title', 'subTitle', 'label', 'menuLabel'].includes(property)) {
    const prefixIsDynamic = obj[property].startsWith('i18n:') || obj[property].startsWith('{{');
    expect(prefixIsDynamic).toBeTruthy();
  }
};

const translationExistsAssertion = (obj: any, property: string) => {
  if (!obj[property].toString().startsWith('i18n:')) return;
  const expectedKey = obj[property].toString().replace('i18n:', '');
  expect(Object.keys(englishTranslations).find(key => key === expectedKey)).toBe(expectedKey);
};
```

#### Language State — Locale-Aware Formatting

```typescript
const { language } = useI18n();

language.current;     // 'en' — ISO code of user's selected language
language.available;   // [{ code: 'en', label: 'English' }, { code: 'fr', label: 'Français' }]
language.setLanguage('fr');  // switch language (triggers re-render)

// Use for locale-aware number/date formatting:
const formatted = new Intl.NumberFormat(language.current).format(amount);
const date = new Intl.DateTimeFormat(language.current).format(new Date(timestamp));
```

Available languages come from the `i18n.languages` account setting — configured per Fluent account.

#### i18n File Structure

```
i18n/
  locales/
    en/
      translation.json    ← flat key-value, source of truth for English
    fr/
      translation.json    ← additional locale (add when l10n required)
```

Webpack `CopyWebpackPlugin` copies `i18n/locales/` to `dist/i18n/` at build time. The Mystique host discovers and loads the correct locale file at runtime based on the user's language setting. Start with `en` only — add locales when requirements demand it.

#### i18n Anti-Patterns

- `translate('key')` without fallback → users see raw key if missing
- Concatenating translated strings → word order differs across languages (`translate('hello') + ' ' + translate('world')` breaks in RTL/agglutinative languages)
- Hardcoded English strings in JSX → invisible to translation pipeline
- `i18n:` prefix in React source code → only valid in manifest JSON, not in component code
- Missing `translation.json` entry for `translateOr` key → fallback works but key is untranslatable to other locales

### useData — Page Query Data Access

```typescript
import { useData } from 'mystique/hooks/useData';

const { data, variables, loading, error } = useData();

data.order;          // page query result, scoped by component's dataSource
variables.orderId;   // query variables from manifest
```

**Use case**: Components that interact with page-level data without needing their own query.

### useEnv — Environment Details

```typescript
import { useEnv } from 'mystique/hooks/useEnv';

const env = useEnv();

env.account;  // Account name
env.app;      // App name: "oms", "store", "servicepoint", or custom
env.api;      // API endpoint URL
```

### useRest / getRest — REST API Calls

```typescript
import { useRest } from 'mystique/hooks/useRest';
import { getRest } from 'mystique/hooks/getRest';

// Hook variant (reactive, auto-fetches)
const [response] = useRest<MyResponse>('/api/v4.1/event/sync', 'POST', eventBody);
// response = { status: 'loading' | 'ok' | 'error', result?: T }

// Promise variant (imperative, for event handlers)
const result = await getRest<MyResponse>('/api/v4.1/event/sync', 'POST', {
  name: 'MyEvent',
  entityType: 'ORDER',
  entityRef: orderRef,
  attributes: { actionedBy: username },
});
```

Fluent API domain is auto-prepended — endpoint starts after domain (e.g., `/api/v4.1/...`). Auth headers injected automatically.

### getApiDownload — File Downloads

```typescript
import { getApiDownload } from 'mystique/hooks/getApiDownload';

const handleDownload = async () => {
  const blob = await getApiDownload('api/v4/consignment/1/labelStream');
  const url = URL.createObjectURL(blob);
  window.open(url);
};
```

### useUserActions / useUserActionForm — Workflow User Actions

```typescript
import { useUserActions } from 'mystique/hooks/useUserActions';
import { useUserActionForm } from 'mystique/hooks/useUserActionForm';

// Retrieve available user actions for the page entity
const actions = useUserActions(); // or useUserActions('orderById')
// actions = { status, result: { entity, userActions: [{ eventName, type, label, confirm }] } }

// Generate a form for a specific user action
const form = useUserActionForm('RequestAppeasement', {
  path: 'orderById',
  extension: {
    postSubmit: { type: 'navigate', link: '/orders/{{id}}' },
  },
  overrides: {
    myField: { label: 'Custom Label', options: [...] },
  },
});
// form = { status, result: { label, hasVisibleFields, form: <ReactElement>, submit: () => void } }
```

### Frame Utilities — Modals, Drawers, Toasts

```typescript
import { pushModal, clearModals } from 'mystique/frame/pushModal';
import { pushDrawer, clearDrawers } from 'mystique/frame/pushDrawer';
import { pushToast } from 'mystique/frame/pushToast';

// Open a modal with data + callback
pushModal({
  title: <Typography variant="h2">{translateOr(['modal.title', 'Confirm'])}</Typography>,
  body: <ConfirmDialogBody data={entityData} onConfirm={handleConfirm} />,
});

// Inside modal body component — close after action
const handleConfirm = () => {
  onConfirm(selectedItem);
  clearModals();  // close modal after action
};

// Toast notifications
pushToast('success', translateOr(['toast.saved', 'Changes saved']), 5000);
pushToast('error', translateOr(['toast.error', 'Operation failed']), 8000);

// Drawer (side panel)
pushDrawer({
  title: <Typography variant="h2">Details</Typography>,
  body: <DetailPanel data={data} />,
});
```

**Key patterns:** Pass data and callbacks as props to modal/drawer body components. Always call `clearModals()` / `clearDrawers()` after the action completes. Toasts auto-dismiss after the timeout (ms).

**`pushModal` works from FieldRegistry fields** — unlike `data` and `useData()`, frame utilities are global and work from any React tree including user action dialogs. Use for "view details" or "select from list" interactions within a field.

**`pushToast` vs MUI `useSnackbar`:**

| API | Source | Use when |
|-----|--------|----------|
| `pushToast` | Mystique frame | Notifying about Mystique operations (saved, submitted, config error) |
| `useSnackbar` | Material-UI 4 | Custom MUI components outside Mystique lifecycle |

Prefer `pushToast` — it integrates with Mystique's notification queue and i18n. Only use MUI `useSnackbar` when building standalone MUI components not connected to Mystique operations.

### Component Loading / Error / Empty Guard Pattern (REQUIRED)

Every component that fetches data must implement the 4-stage guard pattern. Components without guards flash empty content, show raw errors, or crash on null data.

```typescript
const MyComponent: FC<StdProps> = (props) => {
  const [result] = useQuery<DataResponse>(query, { id: entityId });
  const { fetching, data, error } = result;
  const settings = useSettings({ config: 'my.setting.name' });
  const { translateOr } = useI18n();

  // 1. Loading guard — show spinner while any async source loads
  if (fetching || settings.config.status === 'loading') {
    return <div style={{ minHeight: '4em' }}><Loading /></div>;
  }

  // 2. Error guard — handle settings/query errors
  if (settings.config.status === 'error') {
    pushToast('error', translateOr(['error.config', 'Configuration error']), 8000);
    return null;
  }
  if (error) {
    return <Card><CardContent>Error loading data</CardContent></Card>;
  }

  // 3. Data validation guard — handle empty/invalid data
  if (!data?.orderById) {
    return <Card><CardContent>No data available</CardContent></Card>;
  }

  // 4. Success — render with validated data
  return <ActualContent data={data.orderById} config={settings.config.result?.value} />;
};
```

**The `minHeight: '4em'` on the loading state prevents layout shift** when the spinner replaces content. Import `Loading` from `mystique/components/Loading`.

### Data Fetching Strategy (CRITICAL — Most Common Performance Mistake)

Data fetching is where teams cause the most real damage in Mystique SDK components. The hooks are easy to call — the discipline is knowing which one to use and when. Follow the tier hierarchy below strictly.

**EXCEPTION — FieldRegistry fields in user action dialogs:** `useData()` returns empty context and the `data` prop is `undefined`. FieldRegistry fields MUST use `useQuery()` directly. See §"FieldRegistry Data Access in User Action Dialogs" below.

#### Tier 1 — Free (zero network cost, always prefer)

These read data already in memory. No additional API calls. No loading states needed.

| Hook | What it reads | When to use |
|---|---|---|
| `useData()` | Page-level manifest query result (already fetched by host app) | Any data the page query already selected. Access via `data` prop or `useData()`. Scope with `dataSource` in manifest. |
| `useAuth()` | Session data (user, roles, context) | User display name, role-based visibility, current retailer/location context |
| `useSettings()` | Hierarchical settings (cached per session) | Feature flags, dropdown options, per-retailer config, API keys |

**First question for any data need:** Can I get this from the page query that already ran? If yes, use `useData()` and stop.

#### Tier 2 — Cheap (one query, shared across descendants)

When the page query doesn't have what you need, expand it — don't add a separate call.

**Pattern: Add fields to the manifest `data.query` instead of calling `useQuery()`.**

The manifest page query runs once. ALL descendant components share the result via `useData()`. This is a manifest-level change, not a code change.

```
BEFORE (expensive — component calls useQuery):
  Manifest page query: { orderById(id: $id) { ref status } }
  Component code: useQuery("{ orderById(id: $id) { items { edges { node { ref } } } }")
  Result: 2 API calls per page load

AFTER (cheap — expanded page query, component reads via useData):
  Manifest page query: { orderById(id: $id) { ref status items(first: 50) { edges { node { ref quantity } } } } }
  Component code: const { data } = useData()  // data.items already available
  Result: 1 API call per page load
```

**When you cannot modify the page query** (e.g., OOTB manifest fragment you don't control), use a single `useQuery()` at the list-container level and pass data down as props. Never one query per row.

#### Tier 3 — Expensive (additional network call, last resort)

Each `useQuery()` call is an independent GraphQL request. Each `getRest()` is a REST roundtrip. Use only when:
- Data is NOT available from the page query and you cannot modify the page query
- Data is conditional (only fetch when user clicks "show details")
- Cross-entity lookup that doesn't belong in the page query (e.g., looking up a product from within an order page)

```typescript
// Tier 3 is acceptable here — conditional fetch on user action
const [showDetails, setShowDetails] = useState(false);
const [details] = useQuery<Details>(DETAILS_QUERY, showDetails ? { id: entityId } : undefined);
```

#### Anti-Pattern: N+1 Queries

If a list page renders 50 order cards and each `OrderCard` component calls `useQuery()` for supplementary data, that is 50 additional GraphQL requests on page load.

```
BAD:  Page query fetches order list → each OrderCard calls useQuery for order items = 51 API calls
GOOD: Page query fetches order list WITH items → each OrderCard reads via useData = 1 API call
```

**Fix:** Expand the manifest page query. If you cannot, use ONE supplementary `useQuery` at the list-container level, then pass data down as props.

#### GraphQL Union Types — Product Queries

Fluent's `product` field is a union type (`StandardProduct | GroupProduct | VariantProduct`). Querying `product { ref gtin }` directly returns `null` for union-specific fields. **Always use inline fragments:**

```graphql
# BAD — gtin returns null for GroupProduct (no gtin field on that type)
query { orderById(id: $id) { items(first: 50) { edges { node {
  product { ref gtin }
} } } } }

# GOOD — inline fragments handle each product type
query { orderById(id: $id) { items(first: 50) { edges { node {
  product {
    ... on StandardProduct { ref gtin catalogue { ref } attributes { name value } }
    ... on GroupProduct { ref attributes { name value } }
    ... on VariantProduct { ref gtin attributes { name value } }
  }
} } } } }
```

**Reference:** All three Fluent reference plugins (Store, Returns, Analytics) use this pattern for product queries.

#### Anti-Pattern: Unbounded Connections

Every Relay connection (`edges/node` pattern) in Fluent GraphQL supports pagination. **Always use `first: N`:**

```graphql
# BAD — fetches ALL items (could be thousands)
query { orderById(id: $id) { items { edges { node { ref } } } } }

# GOOD — bounded, paginate if needed
query { orderById(id: $id) { items(first: 50) { edges { cursor node { ref } } pageInfo { hasNextPage } } } }
```

**Guidelines:** `first: 20-50` for on-screen lists. `first: 100-200` only for processing. Implement cursor-based pagination when `hasNextPage` is true.

#### Anti-Pattern: Over-Fetching Deep Entity Graphs

Select only fields you display. Fluent entities have deep nested relationships — selecting 3+ levels deep exponentially increases response size:

```graphql
# BAD — 3 levels deep, massive response
query { orderById(id: $id) {
  items(first: 50) { edges { node {
    product { categories { edges { node { name ref } } }
              attributes { edges { node { name value } } } }
  } } }
} }

# GOOD — only fields you render
query { orderById(id: $id) {
  items(first: 50) { edges { node { ref quantity totalPrice } } }
} }
```

If you need product details, fetch on a separate detail page when the user clicks through — not on the list page.

#### Anti-Pattern: Missing Fragment Spreads on Union Types

Fluent's `Product` type is a union (`StandardProduct | VariantProduct`). Querying union-specific fields without fragment spreads returns `null` silently — the query succeeds but fields are missing.

```graphql
# BAD — ref/gtin are union-specific, returns null without fragment
product { name ref gtin }

# GOOD — fragment spreads for each union member
product {
  name
  ... on StandardProduct { ref gtin catalogue { ref } attributes { name value } }
  ... on GroupProduct { ref attributes { name value } }
  ... on VariantProduct { ref gtin attributes { name value } }
}
```

**Known Fluent union types:** `Product` (StandardProduct | GroupProduct | VariantProduct). When querying product fields beyond `name`, always use fragment spreads. `GroupProduct` does NOT have `gtin` — only Standard and Variant do.

#### Component State: UI Only, Never Entity Data

`useState` should hold **UI state**, not entity data:

| Store in state | Let the query system handle |
|---|---|
| Expanded/collapsed, selected tab, modal open/closed | Entity attributes, order items, fulfilments |
| Form input values (before submit) | Settings values |
| Local sort/filter direction | Query results, re-fetch cycles |

Entity data is always potentially stale — another user may change it at any time. Re-fetching via `useQuery` or the manifest query system is the source of truth. Caching entity data in component state risks showing outdated information.

**Exception:** FieldRegistry fields in user action dialogs MUST store entity data locally because `useData()` returns empty. Data is short-lived (dialog lifetime only).

#### Query Cache Behavior (No Shared Cache)

| Hook | Cache behavior |
|---|---|
| `useData()` | Shares the page query result — cached for page lifetime. Navigating away and back re-executes. |
| `useQuery()` | Each instance makes its own fetch. **No shared cache.** Two components calling `useQuery()` with identical query + variables = two separate API calls. |
| `useSettings()` | Cached per session. Settings rarely change mid-session. |

There is no Apollo Client-style normalized cache. This is the strongest argument for Tier 1/2 over Tier 3 — duplicate `useQuery()` calls are never deduplicated.

#### When to Use useReducer Instead of useState

For forms with 3+ interdependent fields (quantity selectors, return item lists, multi-step workflows), `useState` creates spaghetti state. Use `useReducer` with discriminated union actions:

```typescript
// 1. Define typed actions
type Action =
  | { type: 'init'; data: OrderItem[] }
  | { type: 'update'; itemId: string; quantity: number }
  | { type: 'validate' };

// 2. Pure reducer function
const reducer: Reducer<State, Action> = (state, action) => {
  switch (action.type) {
    case 'init':    return { ...state, items: buildInitialState(action.data), init: true };
    case 'update':  return { ...state, items: updateItem(state.items, action.itemId, action.quantity) };
    case 'validate': return { ...state, showValidation: true };
  }
};

// 3. Use in component
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'update', itemId: '123', quantity: 5 });
```

**When to upgrade:** 3+ related state variables that change together, state transitions with business logic (init → edit → validate → submit), undo/redo behavior needed.

#### When to Extract a Custom Hook

Extract to `src/shared/hooks/` when:
1. **3+ stateful variables interacting** — complex state logic obscures render logic
2. **Logic reused across components** — two components with identical fetch + transform
3. **Complex event handlers** — multiple callbacks that depend on shared state

```typescript
// src/shared/hooks/useWavePick.ts
export const useWavePick = (data: PickData): UseWavePickReturn => {
  const [rows, setRows] = useState(data.items);
  const [sortConfig, setSortConfig] = useState<SortConfig>({});
  const { translateOr } = useI18n();

  const onChange = useCallback((event) => { /* update rows */ }, [rows]);
  const onFilter = useCallback((value) => { /* filter rows */ }, [rows]);

  return { rows, onChange, onFilter, sortConfig, totalCount: rows.length };
};
```

**Return a typed interface** (not a tuple). Document with JSDoc. Include loading/error states if the hook fetches data.

### Performance & Responsiveness

#### Memoization

Use `React.memo()` and `useMemo` where they measurably help — not everywhere:

```typescript
// DO memoize: expensive computation over large datasets
const sortedItems = useMemo(() =>
  items.slice().sort((a, b) => a.ref.localeCompare(b.ref)),
  [items]
);

// DO memoize: component receives new object refs on every parent render
const ExpensiveChart = React.memo(({ data }: { data: ChartData[] }) => {
  // heavy SVG rendering
});

// DON'T memoize: simple renders with primitive props
// React.memo adds overhead that exceeds the savings
const StatusBadge = ({ status }: { status: string }) => <span>{status}</span>;
```

**When to `React.memo()`:** Components rendered inside lists (50+ items), components with expensive rendering (charts, tables), components that re-render due to parent state changes they don't use.

**When NOT to:** Simple leaf components, components that almost always receive new props anyway, components rendered once per page.

#### Responsive Design with Material-UI 4

The host app provides MUI 4 responsive utilities. Use them for mobile/tablet adaptation:

```typescript
import { useMediaQuery, useTheme, Grid, Hidden } from '@material-ui/core';

const OrderDetail = ({ data }) => {
  const theme = useTheme();
  const isMobile = useMediaQuery(theme.breakpoints.down('sm'));  // < 600px
  const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md'));

  return (
    <Grid container spacing={isMobile ? 1 : 2}>
      <Grid item xs={12} md={8}>
        {/* Main content — full width on mobile, 2/3 on desktop */}
        {isMobile ? <OrderCardList data={data} /> : <OrderDataTable data={data} />}
      </Grid>
      <Hidden smDown>
        <Grid item md={4}>
          {/* Sidebar — hidden on mobile */}
          <OrderSummaryPanel data={data} />
        </Grid>
      </Hidden>
    </Grid>
  );
};
```

**Key patterns:**
- `useMediaQuery` for conditional rendering — don't render heavy tables on mobile if a card list works
- `<Hidden smDown>` to hide desktop-only sections (sidebars, secondary panels)
- `<Grid>` with `xs`/`sm`/`md`/`lg` breakpoints for responsive column layouts
- Touch targets: ensure clickable areas are at least 48x48px on mobile
- Most OMS App/Store App instances run on desktops or warehouse tablets — mobile optimization is secondary to query efficiency

#### Data Volume on Mobile

If your component supports mobile use cases, consider lighter data on slow connections:

```typescript
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));

// Fewer columns on mobile
const visibleColumns = isMobile
  ? ['ref', 'status']
  : ['ref', 'status', 'createdOn', 'totalPrice', 'customerName'];

// Smaller page size on mobile
const pageSize = isMobile ? 10 : 25;
```

This reduces both the rendered DOM and (if the page query respects the `first` variable) the API response size.

### Module-Level State: Session Safety Rules

Mystique SDK bundles are loaded once per browser tab. The JavaScript module scope persists for the lifetime of the tab — across page navigations, dialog open/close cycles, and even context switches. **Never store mutable per-session data in module-level variables.**

```typescript
// ❌ BAD: Module-level mutable state — persists across dialogs and navigations
let cachedOrderData: any = null;
let formValues: Record<string, string> = {};

const MyField = ({ value, onChange }) => {
  if (!cachedOrderData) cachedOrderData = fetchOrder(); // stale on next dialog open
  formValues.amount = value; // leaks between form instances
};

// ❌ BAD: Module-level singleton store — shared across concurrent dialogs
let globalStore: FormStore | null = null;
export function getOrCreateStore() {
  if (globalStore) return globalStore; // returns SAME instance for every dialog
  globalStore = createStore();
  return globalStore;
}
```

**Why this is dangerous:**
- User A opens a dialog, enters an appeasement amount of $1000, then cancels
- User B opens the same dialog — the module-level store still has $1000 from User A
- Two dialogs open in the same tab (nested modals) share the same singleton
- Context switch (retailer A → retailer B) doesn't reset module state

```typescript
// ✅ GOOD: React state — scoped to component instance, cleaned up on unmount
const MyField = ({ value, onChange }) => {
  const [localData, setLocalData] = useState<OrderData | null>(null);
  useEffect(() => {
    fetchOrder().then(setLocalData);
    return () => setLocalData(null); // cleanup
  }, []);
};

// ✅ GOOD: useRef for per-instance mutable data (not shared across components)
const MyField = ({ value, onChange }) => {
  const storeRef = useRef(createStore()); // one store per component instance
};

// ✅ GOOD: React Context for cross-component sharing within a single dialog tree
const FormStoreContext = React.createContext<FormStore | null>(null);
const DialogWrapper = ({ children }) => {
  const [store] = useState(() => createStore()); // fresh per dialog mount
  return <FormStoreContext.Provider value={store}>{children}</FormStoreContext.Provider>;
};
```

**Rules:**
1. **Module-level constants are fine** — `const QUERY = "..."`, `const FIELD_MAP = { ... }`, regex patterns
2. **Module-level mutable state is never safe** — `let cache = null`, `let store = ...`, `const set = new Set()`
3. **Cross-component state within a dialog:** Use React Context Provider wrapping the dialog tree — auto-cleaned on unmount
4. **FieldRegistry cross-field communication:** If one field needs to share state with another field in the same user action dialog, use React Context or a ref-based store scoped to the dialog's React tree — NOT a module singleton

**Exception:** Module-level caches for truly static, user-independent data (e.g., a parsed schema, compiled regex) are acceptable because they don't vary by session. But entity data, form values, and user-specific state must always be React-managed.

---

## Component Composition Patterns

### Container + View Component Split

OOTB plugins split complex components into Container (logic) + View (presentation):

| File | Responsibility |
|------|---------------|
| `MyComponent.container.tsx` | Hooks, state, data fetching, event handlers |
| `MyComponent.view.tsx` | Pure render, receives all data via props |
| `MyComponent.styles.ts` | `makeStyles` theme-aware styles |

**When to split:** Component has 3+ hooks OR complex state management OR needs independent visual testing. Simple components (1-2 hooks, straightforward render) stay as single files.

```tsx
// MyComponent.container.tsx
const MyComponentContainer = (props: StdProps) => {
  const { data } = useData();
  const [state, dispatch] = useReducer(reducer, initialState);
  return <MyComponentView data={data} state={state} onAction={handler} />;
};

// MyComponent.view.tsx
const MyComponentView = ({ data, state, onAction }: ViewProps) => (
  <Card><CardContent>...</CardContent></Card>
);
```

**Reference:** Store plugin (FulfilmentPack, WavePickList), Returns plugin (ReturnItemList) all use this pattern for their complex FieldRegistry components.

### DynamicValue — Manifest-Configurable Content

`<DynamicValue />` renders a single dynamic attribute (text, image, or component) from manifest config. Keeps components reusable — the manifest configurer controls what is displayed.

```typescript
import { DynamicValue } from 'mystique/components/DynamicValue';

const OrderCard = ({ data, props }) => {
  return (
    <div>
      {props.attributes?.map((attr, i) => (
        <div key={i}>
          <label>{attr.label}</label>
          <DynamicValue attribute={attr} context={data} defaultValue={<span>-</span>} />
        </div>
      ))}
    </div>
  );
};
```

### Children — Manifest-Driven Composition

`<Children />` renders descendant components configured in the manifest. Lets configurers nest arbitrary components inside yours.

```typescript
import { Children } from 'mystique/components/Children';

// Basic: render all descendants from manifest
const Panel = ({ data }) => (
  <div className="custom-panel">
    <Children />
  </div>
);

// Override data context for descendants
const FilteredPanel = ({ data }) => {
  const filtered = data.items.filter(i => i.status === 'ACTIVE');
  return <Children dataOverride={filtered} />;
};

// Render specific descendants by index
const TwoPanelLayout = () => (
  <Grid container>
    <Grid item xs={8}><Children index={0} /></Grid>
    <Grid item xs={4}><Children index={1} /></Grid>
  </Grid>
);

// descendantsOverride — programmatically render manifest component instances
// Use when you need to dynamically compose components from manifest config
import { MystiqueComponentInstance } from 'mystique/manifest';

const DynamicPanel = ({ data, config }) => {
  const instances: MystiqueComponentInstance[] = config.panels.map(p => ({
    component: p.alias,
    dataSource: p.dataSource,
    props: p.props,
  }));
  return <Children descendantsOverride={instances} />;
};
```

### TemplateRegistry.render — Resolve Manifest Templates

Use `TemplateRegistry.render()` to resolve Handlebars template strings in component code:

```typescript
import { TemplateRegistry } from 'mystique/registry/TemplateRegistry';

// props.titleTemplate might be "Order {{ref}} — {{status}}" from manifest
const resolved = TemplateRegistry.render(props.titleTemplate, data);
```

### FieldRegistry.get — Compose Existing Fields

Reuse OOTB field components inside custom composite fields:

```typescript
import { FieldRegistry } from 'mystique/registry/FieldRegistry';

const QuantityField = FieldRegistry.get('number');

// Use in render:
<QuantityField name="qty" label="Quantity" value={qty} onChange={setQty} />
```

### MystiqueSelect — Platform Dropdown Component

`MystiqueSelect` wraps MUI Select with Mystique-compatible styling and behavior. Use instead of raw MUI `<Select>` inside custom components to match platform appearance.

```typescript
import { MystiqueSelect } from 'mystique/components/MystiqueSelect';

<MystiqueSelect
  value={selectedReason}
  onChange={(e) => setSelectedReason(e.target.value as string)}
  options={reasons.map(r => ({ label: r.name, value: r.code }))}
  label="Reason"
  fullWidth
/>
```

**Where used:** ReturnRowExpansion (rejection reason dropdown).

### recursiveValueDecorator — Bulk i18n for Settings-Driven Config

Walks an entire JSON config tree and transforms all string values via a callback. Primary use: translating all labels in a settings-driven configuration at once.

```typescript
import { recursiveValueDecorator } from 'mystique/utils';
import { useI18n } from 'mystique/hooks/useI18n';

const { prefixedTranslate } = useI18n();

// Before: config has { columns: [{ label: "i18n:fc.col.name" }] }
// After: config has { columns: [{ label: "Name" }] }
const translatedConfig = recursiveValueDecorator(settingsConfig, 'string', prefixedTranslate);
```

**When to use recursiveValueDecorator:**
- Settings-driven config with nested `i18n:` prefixed strings (table columns, field labels, dropdown options)
- ANY JSON structure from `useSettings` that contains user-visible text mixed with data keys
- Do NOT use for simple single-value translation — use `prefixedTranslate(value)` directly

**When NOT to use:**
- Component has 1-3 translatable strings → use `translateOr` individually
- Config has no `i18n:` prefixed values → decorator does nothing, adds overhead
- Query result data → use `decorateQueryResult` instead (resolves Handlebars templates, not i18n keys)

**`decorateQueryResult`** — A Mystique utility (exported from `mystique/utils`) that walks a query result object and resolves any Handlebars template strings found in its values using `TemplateRegistry.render()`. Use when settings-driven config contains template expressions that reference query data fields (e.g., a column value configured as `"{{arraySum node.items.edges \"node.requestedQuantity\"}}"`). Unlike `recursiveValueDecorator` which handles `i18n:` string translation, `decorateQueryResult` resolves `{{...}}` Handlebars expressions against the query data context.

**Where used:** ReturnItemList — translates all i18n: prefixed strings in settings-driven field configuration via `recursiveValueDecorator`, then resolves Handlebars template expressions in query results via `decorateQueryResult`.

### Print via window.open + document.write

For printing labels, pick lists, or reports — no PDF library needed:

```typescript
const handlePrint = () => {
  const printWindow = window.open('', '_blank');
  if (!printWindow) return;
  printWindow.document.write(`
    <html>
    <head><style>
      body { font-family: Arial; }
      table { border-collapse: collapse; width: 100%; }
      td, th { border: 1px solid #ddd; padding: 8px; }
      @media print { .no-print { display: none; } }
    </style></head>
    <body>
      <table>${rows.map(r => `<tr><td>${r.ref}</td><td>${r.qty}</td></tr>`).join('')}</table>
    </body>
    </html>
  `);
  printWindow.document.close();
  printWindow.print();
};
```

**Where used:** InlineCompatibility (Store plugin) — prints pick lists and compatibility labels.

### iframe Embedding with postMessage

For embedding external dashboards (Looker, analytics):

```typescript
const Dashboard: FC<{ embedUrl: string }> = ({ embedUrl }) => {
  useEffect(() => {
    const handler = (event: MessageEvent) => {
      if (event.origin !== new URL(embedUrl).origin) return; // security check
      // process dashboard events (filter changes, drill-downs)
    };
    window.addEventListener('message', handler);
    return () => window.removeEventListener('message', handler);
  }, [embedUrl]);

  return (
    <iframe
      src={embedUrl}
      sandbox="allow-scripts allow-same-origin allow-popups"
      style={{ width: '100%', height: '600px', border: 'none' }}
    />
  );
};
```

**Where used:** LookerVisualisation (Analytics plugin).

### Dual Alias Registration (Migration Pattern)

Register both a canonical namespace alias and a legacy short alias to support workflows referencing either name:

```typescript
// Short alias matches workflow attributes[].type; canonical alias for new workflows
FieldRegistry.register(['fc.action.field.returnitems', 'RETURN_ITEMS'], ReturnItemList);
```

Keep both during migration. Remove the legacy alias after all workflows are updated to use the canonical name.

---

## Form Field Development

### FormFieldProps — Complete Interface

The framework passes these props to every FieldRegistry component. `<T>` is the value type your field produces (string, number, object, array).

```typescript
interface FormFieldProps<T> {
  // === Core value contract ===
  value: T;                           // Current field value
  onChange: (value: T | null, options?: OnChangeOptions) => void;
  onBlur?: () => void;                // Fires when field loses focus

  // === Field metadata (from workflow userActions[].attributes[]) ===
  name: string;                       // Attribute name from workflow
  label: string;                      // Display label from workflow
  mandatory?: boolean;                // Whether field is required
  disabled?: boolean;                 // Whether field is read-only

  // === Validation lifecycle ===
  error?: boolean;                    // True when framework detects validation error
  lastSubmit?: number;                // Timestamp — set when user clicks form submit/confirm

  // === Entity context ===
  entityContext?: EntityContext[];     // Current entity chain — see §"entityContext" below
  data?: any;                         // Page query data — UNDEFINED in user action dialogs

  // === Source/defaults (from workflow attribute definition) ===
  source?: string;                    // Source token (e.g., "settings.CANCELLATION_REASON")
  defaultValue?: string;              // Default value (e.g., "", "{{me.username}}")
}

// Entity context structure (entityContext[0] = current entity)
interface EntityContext {
  entity: {
    id: string;                       // Entity ID
    type: string;                     // Entity type: "ORDER", "FULFILMENT", "WAVE", etc.
    ref?: string;                     // Entity ref
  };
  rootEntity?: {                      // Parent entity (for child entity pages)
    id?: string;
    ref?: string;
    type?: string;
  };
}
```

The `lastSubmit`, `entityContext`, and extended `onChange` options are documented in detail in the sections below.

### Field Component Pattern

```typescript
import { FC, useState, useEffect } from 'react';
import { FieldRegistry } from 'mystique/registry/FieldRegistry';

const MyField: FC<FormFieldProps<string>> = ({ value, onChange, disabled, error }) => {
  useEffect(() => {
    if (!value && defaultValue) onChange(defaultValue);
  }, []);

  return (
    <TextField
      value={value || ''}
      onChange={e => onChange(e.target.value)}
      disabled={disabled}
      error={!!error}
    />
  );
};

FieldRegistry.register(['myFieldType', 'company.fields.MyField'], MyField);
```

### onChange API — Value Types and Options

`onChange` is not limited to strings. It accepts any serializable value (objects, arrays, numbers) and an optional second argument with confirmation UI, validation, and button controls.

**Full signature (all options):**
```typescript
onChange(value: T | null, options?: {
  summary?: {
    content: React.ReactElement;  // Custom JSX rendered in the confirmation dialog
    title?: string;
  };
  isInvalid?: boolean;            // When true, blocks form submission (button stays visible)
  disableActionButton?: boolean;  // When true, grays out the submit button entirely
  confirm?: {
    title: string;                // Custom confirmation dialog title
    content: React.ReactElement;  // Replaces standard confirmation with custom JSX
  };
});
```

**`onChange(null)` — signal invalid/not-ready state:**
```typescript
// null tells the form the field value is not ready — distinct from empty string or undefined
if (!dataLoaded || !isValid) {
  onChange(null);  // form cannot submit
} else {
  onChange(buildPayload());  // form can submit
}
```

**onChange(null) as canonical validation gate (OOTB pattern):**

OOTB FieldRegistry components use `onChange(null)` in `useEffect` to signal invalid state to the form framework. This is the standard validation gate — always wrap in `useEffect`, never call `onChange` directly in the render body:

```typescript
useEffect(() => {
  if (isValid) {
    onChange({ name: fieldName, value: validValue });
  } else {
    onChange(null); // Tells form: this field is invalid, block submit
  }
}, [validValue, isValid]);
```

- `onChange(payload)` — field valid, value included in submission
- `onChange(null)` — field invalid, form submit blocked
- Always gate in `useEffect` — never call onChange directly in render (see §"onChange + useEffect — Loop Prevention")

**Structured payload — one field, multiple values:**
```typescript
// Send an object — the framework serializes it into the event attribute
interface WavePayload { count: number; userName: string; fulfilmentTypes: string[] }

const WaveDetailsField: FC<FormFieldProps<WavePayload>> = ({ onChange }) => {
  // ... build payload from queries, user input, auth
  onChange({ count, userName, fulfilmentTypes });
};
```

**Confirmation summary — inject custom JSX into the confirmation dialog:**
```typescript
// Send array of IDs + a summary panel for the confirmation step
onChange(
  selectedRows.map(row => Number(row.id)),
  {
    summary: {
      content: <SelectedItemsSummary items={selectedRows} onRemove={handleRemove} />,
      title: '',
    },
    isInvalid: selectedRows.length > maxLimit,
  }
);
```

**`disableActionButton` — gray out the submit button:**
```typescript
// Used when the field's workflow is not yet complete (e.g., pack not finished)
// Different from isInvalid: disableActionButton grays out the button entirely,
// isInvalid blocks submission but keeps the button visually active
onChange(
  !state.unpackedItems.length && state.valid ? buildPayload(state) : null,
  { disableActionButton: !!state.unpackedItems.length }
);
```

**`confirm` — custom confirmation dialog before submission:**
```typescript
// Replaces the standard confirm/cancel dialog with custom content
// Used for review/summary screens before final submission (e.g., wave pick confirmation)
onChange(pickedItems, {
  confirm: {
    title: 'Confirm Wave Pick',
    content: (
      <PickConfirmation
        rows={rows}
        totalItems={totalRequested}
        totalPicked={totalPicked}
      />
    ),
  },
});
```

**Distinction between `summary` and `confirm`:**

| Option | Behavior | Use when |
|--------|----------|----------|
| `summary.content` | JSX appears IN the standard confirmation dialog alongside confirm/cancel buttons | Review panel (selected items table, calculated totals) |
| `confirm.content` | REPLACES the standard confirmation dialog entirely with your custom JSX | Full custom confirmation screen (pick summary, short-pick warning) |
| `isInvalid` | Blocks form submission, button stays visible and clickable (shows validation errors) | Client-side validation (nothing selected, over limit) |
| `disableActionButton` | Grays out submit button entirely | Workflow not complete (items still being scanned/packed) |

**Reference:** Fluent's own Store plugin uses these patterns:
- `WaveCreationDetails` sends a payload object via `onChange`
- `CreateWaveTable` sends `number[]` with `summary` JSX + `isInvalid`
- `FulfilmentPack` uses `disableActionButton` during pack workflow
- `WavePickListContainer` uses `confirm` for pick review

**Key rules:**
- `onChange(value)` — value can be string, number, object, array, or null. The framework serializes it.
- `onChange(null)` — signals the field is not ready. Form cannot submit.
- `onChange(value, { isInvalid: true })` — blocks submission, shows validation state.
- `onChange(value, { disableActionButton: true })` — grays out submit button entirely.
- `onChange(value, { summary: { content: <JSX /> } })` — JSX renders in the confirmation dialog.
- `onChange(value, { confirm: { title, content: <JSX /> } })` — replaces the entire confirmation dialog.
- Each FieldRegistry field still owns ONE attribute slot. If you need one component to own all attributes, use an action-level component (Pattern C/D in the user action contract).

### FieldRegistry Data Access in User Action Dialogs (CRITICAL)

**FieldRegistry fields in user action dialogs receive NO page data.** This is a fundamental limitation of how Mystique renders user action forms — the dialog is a separate React tree outside the page's data context provider.

| Data source | Result in user action dialog | Works? |
|---|---|---|
| `data` prop | `undefined` | NO — FieldRegistry fields don't receive this prop (unlike ComponentRegistry components) |
| `useData()` hook | `{ context: { data: {} }, dataSource: [], loadingState: "ready" }` | NO — empty context, dialog is outside page data provider |
| `useQuery()` hook | Full GraphQL response | YES — self-contained query, works anywhere |

**Solution: Self-contained `useQuery()` with `entityContext`-first entity ID**

```typescript
import { useQuery } from 'mystique/hooks/useQuery';

// 1. Fallback only when entityContext is missing or you need route-only values
function getOrderIdFromUrl(): string | null {
  const hash = window.location.hash;
  const match = hash.match(/orders\/(\d+)/);
  return match ? match[1] : null;
}

// 2. Define a VALIDATED GraphQL query (validate via MCP graphql.validate FIRST)
const ORDER_ITEMS_QUERY = `
  query OrderItemsForAppeasement($id: ID!) {
    orderById(id: $id) {
      id
      ref
      totalPrice
      items(first: 50) {
        edges { node { id ref price paidPrice totalPrice quantity currency } }
      }
    }
  }
`;

// 3. Fetch data independently inside the field component
const MyField: FC<FieldProps<string>> = ({ value, onChange, entityContext }) => {
  const orderId = useMemo(
    () => entityContext?.[0]?.entity?.id ?? getOrderIdFromUrl(),
    [entityContext]
  );
  const [queryResult] = useQuery<any>(ORDER_ITEMS_QUERY, orderId ? { id: orderId } : undefined);

  // 4. Handle async data arrival — query loads AFTER first render
  const items = useMemo(() => extractItems(queryResult?.data), [queryResult?.data]);
  const [localItems, setLocalItems] = useState<Item[]>([]);

  useEffect(() => {
    if (items.length > 0 && localItems.length === 0) {
      setLocalItems(items);  // Re-sync when data arrives
    }
  }, [items]);

  // ... render with localItems
};
```

**Preferred source:** `entityContext?.[0]?.entity.id` is the default because it is supplied by the framework and is more stable than parsing the route hash. Use URL parsing only as a fallback or when you need extra route values such as `retailerId`.

**MANDATORY: Validate GraphQL queries via MCP before embedding in source.** Call `mcp graphql.validate` with the query string. This catches:
- Fields that don't exist on a type (e.g., `currency` on `Order` — doesn't exist, use `OrderItem.currency`)
- Scalar fields used as objects (e.g., `totalPrice { ... }` when `totalPrice` is `Float`)
- Missing or mistyped field names

**URL hash patterns by entity:**

| Entity | Hash pattern | Regex |
|---|---|---|
| Order | `#orders/:id/:retailerId/:ref` | `/orders\/(\d+)/` |
| Fulfilment | `#fulfilments/:id/:retailerId/:ref` | `/fulfilments\/(\d+)/` |
| Return | `#returns/:id/:retailerId/:ref` | `/returns\/(\d+)/` |
| CreditMemo | `#creditmemo/:id/:retailerId/:ref` | `/creditmemo\/(\d+)/` |

### onChange + useEffect — Loop Prevention (CRITICAL)

**CRITICAL:** `onChange` triggers a parent re-render which re-renders this component. Never call `onChange` in the render body — wrap in `useEffect`:

```typescript
// WRONG — infinite re-render loop
const BadField = ({ onChange, value }) => {
  onChange(value || 'default'); // called every render!
  return <input value={value} />;
};

// CORRECT — useEffect prevents the loop
const GoodField = ({ onChange, value }) => {
  useEffect(() => {
    if (!value) onChange('default');
  }, [value]); // only runs when value changes

  return <input value={value} onChange={(e) => onChange(e.target.value)} />;
};
```

### lastSubmit — Post-Submit Validation Pattern

The `lastSubmit` prop is a numeric timestamp set by the framework when the user clicks submit. Use it to defer validation display until the first submission attempt.

```typescript
const MyField: FC<FormFieldProps<string>> = ({ value, onChange, mandatory, lastSubmit }) => {
  const [showValidation, setShowValidation] = useState(false);

  useEffect(() => {
    if (lastSubmit) setShowValidation(true);
  }, [lastSubmit]);

  const isInvalid = mandatory && !value;

  useEffect(() => {
    onChange(value || null, { isInvalid });
  }, [value, isInvalid]);

  return (
    <TextField
      value={value || ''}
      onChange={e => onChange(e.target.value)}
      error={showValidation && isInvalid}
      helperText={showValidation && isInvalid ? 'This field is required' : ''}
    />
  );
};
```

**Key facts:**
- `lastSubmit` is `undefined` initially, set to `Date.now()` on first submit click
- Use `!!lastSubmit` as "has user tried to submit?" guard
- Combine with `onChange(value, { isInvalid })` to block submission while showing inline errors
- **Reference:** Returns plugin `ReturnItemList` uses `lastSubmit` → `postSubmit` reducer pattern

#### Three-Layer Validation Gates (OOTB Pattern)

OOTB FieldRegistry components implement validation at three layers:

| Layer | Trigger | Mechanism |
|-------|---------|-----------|
| **Real-time** | Every keystroke/selection | `useEffect` → `onChange(null)` if invalid |
| **Pre-submit** | `lastSubmit` timestamp changes | `useEffect([lastSubmit])` → set `showValidation: true` → display error messages |
| **Post-submit** | After successful submission | `useReducer` `postSubmit` action → reset transient state |

```tsx
// Layer 1: Real-time validation (blocks submit silently)
useEffect(() => {
  onChange(isValid ? buildPayload() : null);
}, [selectedItems, isValid]);

// Layer 2: Pre-submit validation display (shows errors to user)
useEffect(() => {
  if (lastSubmit) {  // lastSubmit is a NUMBER (timestamp), not boolean
    dispatch({ type: 'update', payload: { showValidation: true } });
  }
}, [lastSubmit]);

// Layer 3: Post-submit cleanup (via useReducer)
useEffect(() => {
  if (submitted) dispatch({ type: 'postSubmit' });
}, [submitted]);
```

**CRITICAL:** `lastSubmit` is a `number` (Unix timestamp), NOT a boolean. Truthiness check (`if (lastSubmit)`) works correctly — `0`/`undefined` = no submit yet, positive number = submit attempted.

### entityContext — Current Entity Information

The `entityContext` prop provides the current entity's identity and its parent entity on child entity pages.

```typescript
entityContext[0].entity.id     // "12345" — entity ID
entityContext[0].entity.type   // "ORDER" | "FULFILMENT" | "WAVE" | etc.
entityContext[0].entity.ref    // "ORD-001" — entity reference
entityContext[0].rootEntity?.id    // parent entity ID (child pages only)
entityContext[0].rootEntity?.type  // parent entity type
```

**Entity type guard (from ReturnItemList):**
```typescript
const MyField: FC<FormFieldProps<any>> = ({ entityContext, onChange }) => {
  if (!entityContext?.[0]?.entity?.type) return <span>No entity context</span>;
  if (entityContext[0].entity.type !== 'ORDER') return <span>Orders only</span>;
  const orderId = entityContext[0].entity.id;
  // ... use orderId for GraphQL query
};
```

| Rendering context | `entityContext`? | `data` prop? |
|---|---|---|
| User action dialog (FieldRegistry) | Yes | NO |
| Page component (ComponentRegistry) | No (use `useData()`) | Yes |

**Prefer `entityContext` over URL parsing** — it's framework-provided and always correct. Fall back to URL parsing only when you need `retailerId` (not in `entityContext`).

### ComponentRegistry.get — Runtime Component Lookup

`ComponentRegistry.get()` retrieves a registered component at runtime, returning `undefined` if not found. Two main use cases: (1) optional platform components with fallback, (2) composing OOTB components inside custom FieldRegistry fields.

**Pattern 1: Optional component with fallback**
```typescript
import { ComponentRegistry } from 'mystique/registry/ComponentRegistry';

const Scanner = ComponentRegistry.get('fc.util.scanner.hidden');
if (Scanner) {
  return <Scanner onScan={handleScan} />;
} else {
  return <ManualEntryFallback />;
}
```

**Pattern 2: OOTB component composition inside a custom field (IMPORTANT)**

FieldRegistry fields often need to wrap an OOTB component (e.g., QuantityList) with custom state management, settings-driven config, and validation. Retrieve the platform component via `ComponentRegistry.get()`, cast to its props type, and render with your custom props.

```typescript
import { ComponentRegistry } from 'mystique/registry/ComponentRegistry';
import { FC } from 'react';
import type { QuantityListProps } from 'mystique/components/QuantityList';

// Inside your FieldRegistry field component:
const QuantityList = ComponentRegistry.get('fc.quantity.list') as FC<QuantityListProps>;

// Build props from your custom state + settings
const listProps: QuantityListProps = {
  ...fieldConfig,           // from useSettings
  attributes: columns,      // configurable columns
  data: filteredData,       // your custom state
  defaultPageSize: 100,
};

const selectorProps: QuantitySelectorProps = {
  onChange: handleChange,
  validation: { min: 0, max: '{{returnableQty}}' },
  value: { value: '{{unitQuantity.quantity}}', id: '{{orderItemRef}}' },
};

return <QuantityList {...listProps} quantitySelectorProps={selectorProps} scannerProps={barcodeProps} />;
```

This pattern is used by the official Fluent Returns plugin — `ReturnItemList` is a FieldRegistry field that wraps `fc.quantity.list` with custom reducer state, settings-driven columns, and barcode scanning.

**Known platform components:**

| Alias | Purpose | Props type |
|-------|---------|------------|
| `fc.util.scanner.hidden` | Hardware barcode scanner (hidden input) | `ScannerProps` |
| `fc.scanner.camera` | Camera-based barcode scanner (mobile) | `CameraProps` |
| `fc.quantity.list` | Quantity list with increment/decrement + scanner | `QuantityListProps` |
| `fc.mystique.collapsible.text` | Text with collapse/expand | `{ text, charCutoff }` |
| `fc.mystique.collapsible.attributes` | Attribute list with collapse | `{ exclude, collapseOn, collapseLimit }` |

**QuantityListProps quick reference:**
```typescript
interface QuantityListProps extends ListProps {
  quantitySelectorProps: QuantitySelectorProps;  // onChange, validation, value, itemValidation
  attributes: TranslatableAttribute[];            // columns with label + labelFallback
  scannerProps?: BarcodeFilterProps;               // onScan, onFilter, camera config
}
```

### useReducer — State Machine Pattern for Complex Fields

For FieldRegistry fields with 3+ interrelated state values, undo support, or aggregate validation, prefer `useReducer` over multiple `useState`.

```typescript
type PackAction =
  | { type: 'PACK_ITEM'; itemId: string }
  | { type: 'UNDO_LAST' }
  | { type: 'SET_ITEMS'; items: PackItem[] };

function packReducer(state: PackState, action: PackAction): PackState {
  switch (action.type) {
    case 'PACK_ITEM': {
      const item = state.unpackedItems.find(i => i.id === action.itemId);
      if (!item) return state;
      return {
        ...state,
        packedItems: [...state.packedItems, item],
        unpackedItems: state.unpackedItems.filter(i => i.id !== action.itemId),
        lastAction: action.itemId,
      };
    }
    case 'UNDO_LAST': {
      if (!state.lastAction) return state;
      const item = state.packedItems.find(i => i.id === state.lastAction);
      return {
        ...state,
        packedItems: state.packedItems.filter(i => i.id !== state.lastAction),
        unpackedItems: item ? [...state.unpackedItems, item] : state.unpackedItems,
        lastAction: null,
      };
    }
    default: return state;
  }
}

// Sync reducer → onChange
const PackField: FC<FormFieldProps<PackPayload>> = ({ onChange }) => {
  const [state, dispatch] = useReducer(packReducer, initialState);

  useEffect(() => {
    onChange(
      !state.unpackedItems.length ? buildPayload(state) : null,
      { disableActionButton: !!state.unpackedItems.length }
    );
  }, [state.packedItems, state.unpackedItems]);

  return (
    <>
      <ScanInput onScan={(barcode) => dispatch({ type: 'PACK_ITEM', itemId: barcode })} />
      <Button onClick={() => dispatch({ type: 'UNDO_LAST' })}>Undo</Button>
    </>
  );
};
```

| Situation | Use |
|-----------|-----|
| 1-2 independent state values | `useState` |
| 3+ interrelated values | `useReducer` |
| Undo support needed | `useReducer` (track `lastAction`) |
| Complex cross-field validation | `useReducer` (single `VALIDATE` action) |

**Reference:** FulfilmentPack (12 actions), ReturnItemList (state machine), MultiParcelField (polling + state sync)

#### useReducer — Three Standard Actions (OOTB Canonical)

OOTB plugins (Store, Returns) converge on three standard reducer actions for complex FieldRegistry components:

```tsx
type Action =
  | { type: 'init'; payload: InitialData }
  | { type: 'update'; payload: Partial<State> }
  | { type: 'postSubmit' };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'init': return { ...initialState, ...action.payload, initialized: true };
    case 'update': return { ...state, ...action.payload };
    case 'postSubmit': return { ...state, submitted: true };
    default: return state;
  }
}
```

- **init**: Called once when data first loads (in `useEffect` watching data readiness)
- **update**: Called on user interaction (input change, selection)
- **postSubmit**: Called via `lastSubmit` timestamp change to handle post-submission cleanup

Use when: 3+ pieces of interdependent state, init-from-API pattern, or post-submit cleanup needed. For simple fields (1-2 values), `useState` is sufficient. Domain-specific actions (PACK_ITEM, UNDO_LAST, etc.) extend beyond these three — the three standard actions are the baseline.

### User Action Attribute to Field Mapping

Workflow user actions define `eventAttributes` with a `type` property. Mystique auto-selects the matching FieldRegistry entry:

| Workflow `eventAttributes[].type` | FieldRegistry Entry | Input Type |
|---|---|---|
| `TEXT` / `STRING` | text | Single-line text input |
| `TEXTAREA` | textarea | Multi-line text area |
| `NUMBER` / `INTEGER` | number | Numeric input |
| `BOOLEAN` | boolean | Toggle switch |
| `DATE` | date | Date picker |
| `DROPDOWN` | dropdown | Select from options |
| Custom type name | Custom field (if registered) | Your custom component |

Register custom fields: `FieldRegistry.register('myCustomType', MyFieldComponent)`. The form engine auto-selects when a user action has `type: "myCustomType"`.

### Composing Fields from Registry

Custom field components can reuse existing registered fields via `FieldRegistry.get()`:

```typescript
import { FC, useState, useEffect } from 'react';
import { FieldRegistry } from 'mystique/registry/FieldRegistry';

interface StockReservationPayload {
  locationRef: string;
  quantity: number;
}

export const StockReservation: FC<FormFieldProps<StockReservationPayload>> = ({
  onChange, value, disabled
}) => {
  const [locationRef, setLocationRef] = useState(value?.locationRef || '');
  const [quantity, setQuantity] = useState(value?.quantity || 0);
  const QuantitySelector = FieldRegistry.get('number');

  useEffect(() => {
    onChange({ locationRef, quantity });
  }, [locationRef, quantity]);

  return (
    <div>
      <TextField
        label="Location"
        value={locationRef}
        onChange={(e) => setLocationRef(e.target.value)}
        disabled={disabled}
      />
      <QuantitySelector
        name="quantity"
        label="Quantity"
        value={quantity}
        onChange={(val: number) => setQuantity(val)}
      />
    </div>
  );
};
```

### Event Payload Contract Boundary

The event payload (`POST /event/sync`) is the contract boundary between UI and backend. Rules read `event.getAttributes().get("fieldName")` — they are agnostic to how data was collected. **Backend impact is zero** when using custom fields, as long as attribute names match `@EventAttribute` annotations.

**Two override patterns:**
- **Field-level (Pattern A, recommended):** Register custom FieldRegistry type → replaces ONE field. Standard form still auto-renders all other attributes. Zero risk of missing attributes.
- **Full-form (Pattern B):** Manifest `actions` override with `component` → replaces entire form. Must dispatch ALL `@EventAttribute` names rules expect. Pattern B only when the entire interaction model differs.

See `knowledge/platform/user-action-contract.md` §9.

---

## Three Registries

| Registry | Use when | Registration pattern |
|----------|----------|---------------------|
| `ComponentRegistry` | Custom visual component for manifest `"component": "alias"` | `ComponentRegistry.register(['alias'], Component, {category: 'content'})` |
| `FieldRegistry` | Custom form field for user action dialogs | `FieldRegistry.register(['typeName'], Field)` — workflow attribute `"type": "typeName"` maps to this |
| `TemplateRegistry` | Custom Handlebars helper for template strings | `TemplateRegistry.register('helperName', fn)` |

All registrations happen in `src/index.tsx`. Categories for ComponentRegistry: `page`, `layout`, `content`, `filter`.

### Component Category Decision Tree

The `category` parameter in `ComponentRegistry.register()` classifies what role a component plays. Choose based on what the component **does**, not what it looks like.

```
What does the component do?
│
├─ Replaces the entire page layout (full route-level rendering)?
│  → category: 'page'
│  → Receives: StdProps.data (full page query result)
│  → Uses: <Children /> to render descendant components
│  → Manifest: top-level "component" on a MystiquePage
│  → Example: custom dashboard page, specialized detail page
│
├─ Structures/arranges other components (grid, tabs, conditional)?
│  → category: 'layout'
│  → Receives: StdProps.data (scoped by dataSource)
│  → Uses: <Children /> (core purpose — compose descendants)
│  → Manifest: wraps other components in "descendants"
│  → Example: custom tab container, split pane, collapsible section
│
├─ Displays information or provides an interactive widget?
│  → category: 'content'  (MOST COMMON — default choice)
│  → Receives: StdProps.data (scoped by dataSource) + custom props
│  → Uses: <DynamicValue />, TemplateRegistry.render(), useData()
│  → Manifest: leaf component in "descendants" tree
│  → Example: attribute card, chart, map, custom list, data visualization
│
└─ Modifies query variables to filter/sort page data?
   → category: 'filter'
   → Receives: StdProps.data + access to useData().variables
   → Uses: useData().variables.setVariables() to change query params
   → Manifest: placed alongside fc.list or other data-bound components
   → Example: date range picker, status filter, custom search input
```

**When in doubt, use `content`.** It's the correct choice for 80%+ of custom components.

### Root Wrapper Selection (depends on component purpose)

Not every `content` component needs a `<Card>` wrapper. The right root element depends on what the component does:

| Component Purpose | Root Wrapper | Example |
|---|---|---|
| **Visual card inside `fc.page.section`** | `<Card>` + `<CardContent>` | Attribute display, summary cards |
| **Edge-to-edge content inside card** | `<Card>` (NO `<CardContent>`) | Maps, full-width tables, iframes |
| **Full section/panel (tabs, multi-panel)** | MUI `<Box>` or `<Grid container>` | UserManagement (AppBar + Tabs + Grid) |
| **Invisible utility (side effects only)** | Returns `null` | LogoutMonitor, session poller |
| **Form field (FieldRegistry)** | No wrapper — inside dialog | Custom fields |
| **Filter** | No wrapper — modifies variables | Query filters |

**Card is MANDATORY only when the component sits as a visual card inside `fc.page.section`.** Without Card, the component renders at 0px/1px width because `fc.page.section` only auto-wraps OOTB components in Grid items. For full-panel content (tabs, dashboards), use `<Box>` or `<Grid container>`. For invisible utilities, return `null`.

```tsx
import { Card, CardWidth } from 'mystique/components/Card';
import { CardContent } from 'mystique/components/CardContent';

// Standard props pattern — always accept title + width from manifest
interface MyComponentProps extends StdProps {
  title?: string;
  width?: CardWidth;  // 'quarter' | 'third' | 'half' | 'two-thirds' | 'full' | 1-12
}

// Pass title and width to Card
const MyComponent: React.FC<MyComponentProps> = ({ data, title, width }) => (
  <Card title={title || 'Default Title'} width={width}>
    <CardContent>
      {/* Padded content (text, fields, attributes) */}
    </CardContent>
  </Card>
);
```

**Edge-to-edge content (maps, tables, iframes):** Use `<Card>` but skip `<CardContent>` — the CardContent docs say "Don't use for items that should go to the edge of the Card, like tables or maps."

```tsx
const MapComponent: React.FC<MapProps> = ({ data, title, width }) => (
  <Card title={title || 'Map'} width={width}>
    {/* No CardContent — map goes edge-to-edge inside the card */}
    <div ref={mapRef} style={{ width: '100%', height: '300px' }} />
  </Card>
);
```

**What Card provides:** Title header (h3, uppercase, matches OOTB cards), MUI Paper elevation, Grid item sizing from `width` prop, consistent spacing with sibling cards.

**Anti-pattern for card-style components:** Using raw `<div>`, `<Box>`, or `<Grid item>` as root for components that should render as cards inside `fc.page.section` — these don't participate in Mystique's page section layout.

**Valid non-Card patterns:** `<Box>` or `<Grid container>` for full-panel content components (tabs, dashboards, multi-column layouts). `null` return for invisible utility components. No wrapper for FieldRegistry and filter components.

### Card Inner Div Conventions (OOTB Pattern)

OOTB components inside `<Card>` use a consistent inner `<div>` with:
- `minHeight: '60px'` — prevents visual collapse on empty/loading states
- `margin: '8px'` — consistent internal padding separate from CardContent

```tsx
<Card>
  <CardContent>
    <div style={{ minHeight: '60px', margin: '8px' }}>
      {/* Component content */}
    </div>
  </CardContent>
</Card>
```

Apply to all `content` category components inside `fc.page.section`. Not needed for FieldRegistry (no Card wrapper) or invisible utilities (return `null`).

**Category applies to ComponentRegistry only.** FieldRegistry and TemplateRegistry do not use categories. Form fields are user-action-scoped; template helpers are expression-scoped.

**Wrong category — what breaks:**

| Mistake | What happens |
|---------|-------------|
| `filter` registered as `content` | `setVariables()` still works technically, but tooling/validation won't flag missing variable bindings |
| `content` registered as `page` | Component receives unscoped data instead of `dataSource`-scoped data — renders wrong or empty |
| `layout` registered as `content` | `<Children />` still renders, but manifest validators won't enforce that descendants are required |
| `page` registered as `layout` | `useUserActions()` returns empty — no action buttons render |

### Props Flow Per Category (from @types/mystique/registry.d.ts)

All components receive `StdProps & T` where `StdProps = { data?: any }` and `T` is your custom props interface.

| Category | `data` contains | Typical hooks | Uses `<Children />`? | Uses `<DynamicValue />`? |
|----------|----------------|---------------|---------------------|------------------------|
| `page` | Full page query result (unscoped) | useData, useUserActions | Yes (mandatory — renders page content) | Rarely |
| `layout` | Scoped by parent's `dataSource` | useData | Yes (mandatory — core purpose) | No |
| `content` | Scoped by parent's `dataSource` | useQuery, useSettings, useAuth | Optional (for nesting) | Yes (common) |
| `filter` | Scoped by parent's `dataSource` | useData (for setVariables) | No | No |

**`dataSource` scoping chain**: When a manifest component has `"dataSource": "orderById"`, Mystique extracts `data.orderById` and passes it as `data` to that component and its descendants. Nested `dataSource` values chain:

```
Page query returns: { orderById: { ref: "ORD-1", items: { edges: [{ node: {...} }] } } }

Component tree in manifest:
  fc.page                              → data = { orderById: { ref: "ORD-1", items: ... } }
    └─ content (dataSource: "orderById")
         → data = { ref: "ORD-1", items: { edges: [...] } }
         └─ content (dataSource: "items")
              → data = { edges: [{ node: {...} }] }
```

**`<Children />` indexed slots**: Layout components can place specific descendants by index — `<Children index={0} />` renders the first descendant, `<Children index={1} />` the second. Without `index`, all descendants render in order. Use `dataOverride` to transform data for children: `<Children dataOverride={filteredData} />`.

**`filter` re-fetch behavior**: When a filter calls `useData().variables.setVariables(newVars, true)`, the host re-executes the page query with merged variables. All sibling components on the same page re-render with the new data. Second argument `true` = merge with existing variables; `false` = replace entirely.

**`page` owns user actions**: Only `page` components should call `useUserActions()` — they own the entity detail rendering where workflow action buttons appear.

### Manifest JSON → Component Props Mapping

```
Manifest JSON                          Component receives
────────────────────────────────────   ─────────────────────────────────────
"component": "acme.content.card"    →  (selects the registered component)
"dataSource": "orderById"           →  { data: queryResult.orderById }
"props": { "title": "Order" }       →  { title: "Order" }  (spread as props)
"descendants": [...]                →  (not a prop — access via <Children />)
"roles": ["ADMIN"]                  →  (not a prop — Mystique hides if role check fails)
```

**`StdProps` is minimal**: `{ data?: any }`. The `data` shape depends entirely on the page query and `dataSource` chain. Always document expected shape in your component interface:

```typescript
interface OrderCardData {
  ref: string;
  status: string;
  totalPrice: { amount: number; currency: { alphabeticCode: string } };
}

interface OrderCardProps extends StdProps {
  data?: OrderCardData;  // override the `any` with expected shape
  title?: string;
}
```

### Code Examples by Category

#### `page` — Full Page Replacement

```typescript
import { Children } from 'mystique/components/Children';
import { useData } from 'mystique/hooks/useData';
import { useUserActions } from 'mystique/hooks/useUserActions';
import { StdProps } from 'mystique/registry/ComponentRegistry';

interface CustomPageProps extends StdProps {
  title: string;
  showActions?: boolean;
}

// Page components receive UNSCOPED data (full query result)
const CustomOrderPage: FC<CustomPageProps> = ({ data, title, showActions }) => {
  const { variables } = useData();     // route params: variables.current.id
  const actions = useUserActions();     // workflow user action buttons

  return (
    <div>
      <h1>{title}</h1>
      {showActions && actions.status === 'ok' && (
        <ActionButtons actions={actions.result.userActions} />
      )}
      <Children />  {/* mandatory: renders manifest descendants */}
    </div>
  );
};

ComponentRegistry.register(['acme.page.custom-order'], CustomOrderPage, { category: 'page' });
```

**Manifest:**
```json
{ "type": "page", "path": "/custom-order/:id", "component": "acme.page.custom-order",
  "data": { "query": "query ($id: ID!) { orderById(id: $id) { ref status } }" },
  "props": { "title": "Custom Order", "showActions": true },
  "descendants": [{ "component": "acme.content.order-details", "dataSource": "orderById" }] }
```

#### `layout` — Structural Container

```typescript
import { Children } from 'mystique/components/Children';
import { Grid } from '@material-ui/core';

interface TwoColumnProps extends StdProps {
  leftWidth?: number;
}

// Layout components compose descendants — they DON'T render data directly
const TwoColumn: FC<TwoColumnProps> = ({ leftWidth = 4 }) => {
  return (
    <Grid container spacing={2}>
      <Grid item xs={leftWidth}>
        <Children index={0} />   {/* first descendant in left column */}
      </Grid>
      <Grid item xs={12 - leftWidth}>
        <Children index={1} />   {/* second descendant in right column */}
      </Grid>
    </Grid>
  );
};

ComponentRegistry.register(['acme.layout.two-column'], TwoColumn, { category: 'layout' });
```

#### `content` — Display Information (most common)

```typescript
import { Card } from 'mystique/components/Card';
import { CardContent } from 'mystique/components/CardContent';
import { DynamicValue, DynamicAttribute } from 'mystique/components/DynamicValue';

interface AttributeCardProps extends StdProps {
  title?: string;
  attributes?: DynamicAttribute[];
}

// Content components receive SCOPED data and render it
const AttributeCard: FC<AttributeCardProps> = ({ data, title, attributes }) => {
  // data is scoped by this component's dataSource in the manifest
  // e.g., if dataSource="orderById", data = queryResult.orderById
  return (
    <Card title={title}>
      <CardContent>
        {attributes?.map((attr, i) => (
          <div key={i}>
            <DynamicValue attribute={attr} context={data} defaultValue={<span>-</span>} />
          </div>
        ))}
      </CardContent>
    </Card>
  );
};

ComponentRegistry.register(['acme.content.attribute-card'], AttributeCard, { category: 'content' });
```

#### `filter` — Modify Query Variables

```typescript
import { useData } from 'mystique/hooks/useData';
import { Select, MenuItem } from '@material-ui/core';

interface StatusFilterProps extends StdProps {
  options: Array<{ label: string; value: string }>;
}

// Filter components modify query variables — minimal UI, maximum data impact
const StatusFilter: FC<StatusFilterProps> = ({ options }) => {
  const { variables } = useData();

  const handleChange = (e: React.ChangeEvent<{ value: unknown }>) => {
    // setVariables re-executes the page query with new params
    variables.setVariables({ status: e.target.value as string }, true);
  };

  return (
    <Select value={variables.current.status || ''} onChange={handleChange} displayEmpty>
      <MenuItem value="">All Statuses</MenuItem>
      {options.map(opt => (
        <MenuItem key={opt.value} value={opt.value}>{opt.label}</MenuItem>
      ))}
    </Select>
  );
};

ComponentRegistry.register(['acme.filter.status-filter'], StatusFilter, { category: 'filter' });
```

### Component Registration Patterns

```typescript
// Standard display component — content category (most common)
import { ComponentRegistry } from 'mystique/registry/ComponentRegistry';
ComponentRegistry.register(['acme.content.order-card'], OrderCard, { category: 'content' });

// Page-level component — replaces the entire page layout
ComponentRegistry.register(['acme.page.custom-dashboard'], Dashboard, { category: 'page' });

// Layout component — structures other components
ComponentRegistry.register(['acme.layout.split-pane'], SplitPane, { category: 'layout' });

// Filter component — modifies query variables
ComponentRegistry.register(['acme.filter.date-range'], DateRangeFilter, { category: 'filter' });
```

### FieldRegistry Registration Patterns

```typescript
import { FieldRegistry } from 'mystique/registry/FieldRegistry';

// Pattern 1: Single alias (simple — type name IS the alias)
// Workflow attribute "type": "appeasementAmount" maps directly
FieldRegistry.register(['appeasementAmount'], AppeasementAmountField as any);

// Pattern 2: Dual alias (recommended — type name + namespaced alias)
// Workflow uses short name; manifest/code uses namespaced name for clarity
FieldRegistry.register(['appeasementAmount', 'acme.fields.appeasement-amount'], AppeasementAmountField as any);

// Pattern 3: Multiple type names (backward compatibility)
// Old workflows use 'stockReservation', new ones use 'acme.fields.stock-reservation'
FieldRegistry.register(['stockReservation', 'acme.fields.stock-reservation'], StockReservation as any);
```

**Why `as any`?** The SDK signature expects `FC<FormFieldProps<T>>`, but real projects define standalone `FieldProps<T>` interfaces instead of extending `FormFieldProps<T>`. Reasons: (1) `FormFieldProps<T>` requires mandatory `name`/`label` fields that add noise, (2) custom fields often need `data` (page query context) which `FormFieldProps<T>` doesn't include, (3) the `onChange` signature differs (`FormFieldProps<T>` has an `OnChangeOptions` second parameter). The cast is safe because Mystique passes compatible props at runtime.

**FieldRegistry naming convention**: The first alias should match the workflow attribute `type` value exactly (often camelCase for OOTB compatibility). The second alias follows `<namespace>.fields.<kebab-name>` for code clarity.

### TemplateRegistry Registration

```typescript
import { TemplateRegistry } from 'mystique/registry/TemplateRegistry';

// TemplateRegistry.register() accepts string | string[] (unlike Component/Field which require string[])
TemplateRegistry.register('acme.helpers.format-currency', formatCurrency);

// Multiple aliases also supported
TemplateRegistry.register(['formatCurrency', 'acme.helpers.format-currency'], formatCurrency);
```

### Alias Naming Rules

| Rule | Detail |
|------|--------|
| **Reserved prefix `fc.`** | NEVER use `fc.*` aliases — reserved for Fluent OOTB components. Your component will silently override the built-in one. |
| **ComponentRegistry format** | `<namespace>.<category>.<kebab-name>` — e.g., `acme.content.order-card` |
| **FieldRegistry format** | Primary: camelCase type name matching workflow `type`. Secondary: `<namespace>.fields.<kebab-name>` |
| **TemplateRegistry format** | `<namespace>.helpers.<kebab-name>` — e.g., `acme.helpers.format-currency` |
| **Namespace derivation** | See table below. Must be unique per deployment — collision with another plugin's namespace causes silent override. |
| **kebab-case for aliases** | Use `order-card` not `OrderCard` or `orderCard` in component aliases |
| **Single canonical alias** | One component = one primary alias. Additional aliases only for backward compatibility. |
| **No dots in name segment** | `acme.content.order-card` is 3 segments (namespace.category.name). `acme.content.order.card` is 4 — avoid. |

### Namespace Derivation Guide

The `<namespace>` segment identifies WHO built the component. Choose based on context:

| Context | Namespace Strategy | Example |
|---------|-------------------|---------|
| **Fluent Commerce OOTB** | `fc` (reserved — never use) | `fc.list`, `fc.page`, `fc.card.attribute` |
| **Partner / SI company** | Company abbreviation (2-6 chars, lowercase) | `acme`, `tcs`, `dxc`, `wipro` |
| **Client / brand** | Brand abbreviation or project code | `nike`, `coles`, `woolies`, `proj1` |
| **Feature-scoped** | Feature or domain name | `appeasement`, `returns`, `sourcing` |
| **Internal / prototype** | Team or project prefix | `demo`, `poc`, `hackathon` |

**Rules for choosing a namespace:**
1. **Lowercase, no dots, no special characters** — the namespace IS the first dot-segment
2. **Short but unique** — 2-6 characters preferred, max ~12
3. **Stable** — once deployed, the namespace is baked into manifests and workflows. Renaming requires updating every reference.
4. **One namespace per plugin bundle** — all components in one `src/index.tsx` share the same namespace
5. **Ask the partner/client** — if building for a customer, ask what prefix they want. Default to their company abbreviation.

**Example — partner "Acme Solutions" building for client "SuperMart":**
```typescript
// Option A: Partner namespace (recommended for reusable components)
ComponentRegistry.register(['acme.content.order-timeline'], OrderTimeline, { category: 'content' });

// Option B: Client namespace (recommended for client-specific components)
ComponentRegistry.register(['supermart.content.loyalty-card'], LoyaltyCard, { category: 'content' });

// Option C: Feature namespace (recommended when one plugin = one feature)
ComponentRegistry.register(['appeasement.content.create-form'], CreateForm, { category: 'content' });
```

---

## Manifest TypeScript Patterns

### gql Tag — Compile-Time Query Validation (OOTB Pattern)

OOTB manifest TypeScript files use the `gql` tag with `parse()` for compile-time GraphQL validation:

```tsx
import gql from 'graphql-tag';
import { parse } from 'graphql';

const ORDER_QUERY = gql`
  query orderById($orderId: ID!) {
    orderById(id: $orderId) {
      id ref status
      orderItems { edges { node { id ref quantity } } }
    }
  }
`;

// parse() validates syntax at build time — catches typos before deployment
const parsed = parse(ORDER_QUERY.loc?.source?.body ?? '');
```

Use in manifest `.ts` files that define GraphQL queries (under `manifests/`). NOT needed in runtime component code (use `useQuery` with string queries there). The SDK ships with `graphql-tag` and `graphql` as dev dependencies for this purpose.

**Reference:** All OOTB reference plugins (Store, Returns, Analytics) include manifest TypeScript files with `gql` tag queries under `manifests/`. The SDK's `Manifest.test.ts` validates these queries at test time.

---

## Deployment Notes

See the **Build & Development Lifecycle** section in SKILL.md for the full dev cycle, production deployment, and hosting options.

### Vercel Deployment Config (Optional)

```json
{
  "version": 2,
  "builds": [{ "src": "dist/**", "use": "@vercel/static" }],
  "routes": [{ "src": "/(.*)", "dest": "/dist/$1" }]
}
```

### Key Facts

- **Fluent does NOT host custom bundles.** Self-host on Vercel, S3, CDN, or any static server.
- **Webpack externals: React, ReactDOM, Material-UI NOT bundled.** Provided by host app at runtime.
- **`@material-ui/lab` is also available** at runtime via `window.MaterialUI` (same global as core). Components like `Autocomplete`, `ToggleButton`, `Alert`, `Skeleton` are accessible. Add `@material-ui/lab` to both `dependencies` (for types/Storybook) and webpack externals (reroute to `window["MaterialUI"]`).
- **CORS headers essential** for local dev (`Access-Control-Allow-Origin: *`).

### Localhost Plugin Troubleshooting

When loading a local SDK plugin from `http://localhost:3001/` into an OMS App served over HTTPS, Chrome's **Private Network Access** (PNA) policy may block the request. Symptoms:

- `version.json` request shows `net::ERR_FAILED` in network tab
- Console error: `Access to fetch at 'http://localhost:3001/...' has been blocked by CORS policy: Permission was denied for this request to access the loopback address space`
- Other plugins (OMS, returns, inventory) load fine — only localhost fails

**Fix 1 — Dev server header (required):** The webpack dev server must include `Access-Control-Allow-Private-Network: true` in its CORS headers. This is already included in the scaffold template.

**Fix 2 — Chrome flag (if Fix 1 alone doesn't work):** Launch Chrome with `--disable-features=PrivateNetworkAccessRespectPreflightResults`. For Chrome DevTools MCP, add to `.mcp.json` args: `"--chrome-flags=--disable-features=PrivateNetworkAccessRespectPreflightResults"`.

**Fix 3 — Chrome DevTools MCP headless:** Headless Chrome may enforce stricter PNA rules. Remove `--headless` from `.mcp.json` args when testing localhost plugins.

---

## Material-UI Constraint

Host OMX app ships **Material-UI v4.12.4** as a global (externalized via webpack).

**Default (v4 externalized):** Import from `@material-ui/core` — zero bundle cost. Use `makeStyles`, `useTheme`, `withStyles`.

**Bundled v5 (opt-in):** You CAN add `@mui/material` as a bundled dependency for `sx` prop, v5 components, and `@mui/x-date-pickers`. Do NOT externalize it. Increases bundle size but is valid for newer projects. Many production implementations mix v4 (externalized) + v5 (bundled).

Theme colors: primary `#00A9E0`, secondary (brand purple), plus `success`, `error`, `warning`, `info`. Access via `makeStyles`:

```typescript
const useStyles = makeStyles((theme) => ({
  root: { color: theme.palette.primary.main },
  error: { color: theme.palette.error.main },
}));
```

---

## CSS Class Name Scoping

Multiple SDK plugins run in the same page. Without scoping, MUI's `makeStyles` generates colliding class names (`jss1`, `jss2`...) across plugins.

```typescript
// In your plugin's root (src/index.tsx or App wrapper)
import { StylesProvider, createGenerateClassName } from '@material-ui/core/styles';

const generateClassName = createGenerateClassName({
  productionPrefix: 'acme',  // Your plugin prefix — must be unique across all plugins
  seed: 'acme',
});

// Wrap your component tree
const App = () => (
  <StylesProvider generateClassName={generateClassName}>
    <MyPluginContent />
  </StylesProvider>
);
```

**Without this**, class names collide and styles bleed between plugins. Every SDK plugin should wrap its root in `StylesProvider`.

---

## TemplateRegistry — Custom Helpers & Built-In Reference

### API Surface (from `registry.d.ts`)

```typescript
interface ITemplateRegistry {
  register: (alias: string | string[], fn: (...args: any[]) => any) => void;
  render: (template?: string, context?: any, options?: any) => string;
  noEscape: (value: any) => unknown;  // Wrap HTML output to prevent escaping
}
```

- `register()` — accepts single string OR string[] (unlike Component/Field which require arrays)
- `render()` — resolves Handlebars template with context data. Used inside React components.
- `noEscape()` — wraps HTML strings so `render()` outputs raw HTML instead of escaped text

### Naming Convention

Helper aliases follow `<namespace>.helpers.<kebab-name>`:

```typescript
TemplateRegistry.register('acme.helpers.format-currency', formatCurrency);
//                         ^^^^^  ^^^^^^^  ^^^^^^^^^^^^^^^
//                         namespace segment  kebab-name
```

The `helpers` segment distinguishes template helpers from `content`/`page`/`layout`/`filter` (ComponentRegistry) and `fields` (FieldRegistry). This is a convention, not enforced by the platform — but following it prevents alias collisions across registries and makes manifest references self-documenting.

### Parameter Difference from Other Registries

| Registry | First parameter | Example |
|----------|----------------|---------|
| `ComponentRegistry.register()` | `string[]` (array required) | `(['acme.content.card'], Component, {category})` |
| `FieldRegistry.register()` | `string[]` (array required) | `(['acme.fields.calculator'], Field)` |
| `TemplateRegistry.register()` | `string \| string[]` (string or array) | `('acme.helpers.format-currency', fn)` |

This means single-alias helpers can skip the array wrapper. Use `string[]` only when registering multiple aliases for backward compatibility.

> **Full helper reference:** See `knowledge/frontend/mystique-templates.md` for the complete built-in helper catalog, template expression syntax, and advanced patterns (block helpers, nested expressions, conditional rendering).

### Helper Function Signature

Helpers follow the [Handlebars helper API](https://handlebarsjs.com/). The function receives positional arguments from the template, plus a Handlebars `options` object as the last parameter:

```typescript
// No parameters — called as {{currentTimezone}}
TemplateRegistry.register('currentTimezone', () =>
  Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone || 'UTC'
);

// With parameters — called as {{formatCurrency 99.99 "USD"}}
TemplateRegistry.register('acme.helpers.format-currency', (amount: number, currency: string) =>
  new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
);

// With noEscape — returns raw HTML, called as {{statusBadge "ACTIVE"}}
TemplateRegistry.register('acme.helpers.status-badge', (status: string) => {
  const colors: Record<string, string> = { ACTIVE: '#4caf50', PENDING: '#ff9800', CANCELLED: '#f44336' };
  return TemplateRegistry.noEscape(
    `<span style="color:${colors[status] || '#999'}">${status}</span>`
  );
});

// With Handlebars options — block helper, called as {{#ifRole "ADMIN"}}...{{/ifRole}}
TemplateRegistry.register('ifRole', function(roleName: string, options: any) {
  // `this` is the template context; `options.fn` renders the block content
  const roles = this.roles || [];
  return roles.includes(roleName) ? options.fn(this) : '';
});
```

**Key rules:**
- Helpers MUST be pure functions (no React hooks — helpers run outside React lifecycle)
- Return a string (or `noEscape()` wrapped string for HTML)
- Block helpers use `function` (not arrow) to access `this` context
- Async helpers are NOT supported — Handlebars is synchronous

### When to Use: Helper vs Component vs Field

| Question | Answer | Build |
|----------|--------|-------|
| Need to transform a **data value** for display? (format currency, date, status) | Yes | **Template Helper** (`TemplateRegistry`) |
| Need a complete **visual element** with layout? (card, chart, timeline) | Yes | **Component** (`ComponentRegistry`) |
| Need a custom **form field** for user action dialogs? (multi-select, calculator) | Yes | **Field** (`FieldRegistry`) |
| Need conditional rendering of OOTB components? | Yes | **Manifest config** (`fc.conditional`, no code needed) |

**Technical distinction:**

| Aspect | Template Helper | Component | Field |
|--------|-----------------|-----------|-------|
| Input | Positional args from `{{helper arg1 arg2}}` | React props (`data`, custom props) | `FormFieldProps` (`value`, `onChange`) |
| Output | String (or noEscape HTML) | React JSX | React JSX with form integration |
| React hooks | NO (pure function) | YES | YES |
| Where used | Manifest template strings | Manifest `"component": "alias"` | Workflow attribute `"type": "alias"` |
| Complexity | Simple formatting | Full UI | Form-aware UI |
| Registered in | `src/index.tsx` | `src/index.tsx` | `src/index.tsx` |

**Decision rule:** If you don't need React hooks, state, or layout — it's a helper. If you do — it's a component or field.

### Registration & Deployment

Register in `src/index.tsx` alongside component/field registrations:

```typescript
import { TemplateRegistry } from 'mystique/registry/TemplateRegistry';

export const initialiseRegistry = (): void => {
  // Components
  ComponentRegistry.register(['acme.content.order-card'], OrderCard, { category: 'content' });

  // Fields
  FieldRegistry.register(['acme.fields.amount-calculator'], AmountCalculator);

  // Template helpers
  TemplateRegistry.register('acme.helpers.format-currency', formatCurrency);
  TemplateRegistry.register('currentTimezone', () =>
    Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone || 'UTC'
  );
};
initialiseRegistry();
```

**Deployment checklist:**
1. Register helper in `src/index.tsx` in `initialiseRegistry()`
2. `yarn build` — helper is bundled into `dist/bundle.[hash].js`
3. Deploy bundle to CDN (Vercel, S3, etc.)
4. Add plugin to **root manifest** `plugins[]`: `{ "type": "url", "src": "https://cdn/bundle.[hash].js" }`
5. Use helper in any manifest template string: `"title": "{{acme.helpers.format-currency totalPrice currency}}"`
6. Helpers are available globally once the plugin loads — all fragments inherit them

### Using TemplateRegistry.render() in Components

Call `render()` inside React components to resolve template strings from settings or manifests:

```typescript
import { TemplateRegistry } from 'mystique/registry/TemplateRegistry';

const OrderCard = ({ data }) => {
  // Resolve settings-driven column labels that may contain {{helpers}}
  const columns = settingsConfig.columns.map(col => ({
    ...col,
    label: prefixedTranslate(TemplateRegistry.render(col.label, data)),
  }));

  return <Table columns={columns} />;
};
```

**Pattern from Store plugin (CreateWaveTable):** Settings define template strings like `"{{arraySum node.items.edges "node.requestedQuantity"}}"`. The component calls `TemplateRegistry.render(value, data)` to resolve them at render time.

### Testing Custom Helpers

```typescript
// src/helpers/__tests__/formatCurrency.test.ts
import { formatCurrency } from '../formatCurrency';

describe('formatCurrency helper', () => {
  it('formats USD', () => {
    expect(formatCurrency(99.99, 'USD')).toBe('$99.99');
  });
  it('handles zero', () => {
    expect(formatCurrency(0, 'USD')).toBe('$0.00');
  });
});

// Testing TemplateRegistry.render() in component tests
import { TemplateRegistry } from 'mystique/registry/TemplateRegistry';

jest.spyOn(TemplateRegistry, 'render').mockImplementation(
  (template, context) => template || ''
);
```

### Built-In Platform Helpers (50)

These are provided by Mystique — do NOT re-register them. Use directly in manifest template strings.

| Category | Helpers |
|----------|---------|
| **Comparison** | `eq`, `ne`, `gt`, `lt`, `gte`, `lte` |
| **Logic** | `and`, `or`, `not`, `if`, `unless`, `in`, `switch`, `hasRole`, `firstDefinedValue`, `anyMatch` |
| **Iteration** | `each`, `with`, `lookup` |
| **String** | `capitalise`/`capitalize`, `humanify`, `toLowerCase`, `toUpperCase`, `concat`, `stringify`, `substring`, `replace`, `split`, `join`, `includes`, `startsWith`, `endsWith`, `preserveWhitespace`, `partialStringMatch`, `decodeUrlParams`, `currentUrlParams` |
| **Placeholder** | `placeholderText`, `placeholderDate`, `placeholderArray` |
| **Date** | `dateFormat`, `dateStringFormatter`, `dateTimeFormatter`, `dateAdd`, `dateSubtract`, `dateRelative`, `dateFormatByLocale` |
| **Number** | `currency`, `formatNumber`, `add`, `subtract`, `multiply`, `divide`, `pow`, `math` |
| **Array** | `len`, `arraySum`, `length`, `pluralize` |
| **Special** | `barcode`, `json`, `i18n` |

**Examples from Store plugin manifests:**
```handlebars
{{arraySum node.items.edges "node.requestedQuantity"}}
{{relativeDateFromNow node.expiryTime}}
{{partialStringMatch value node.order.customer.firstName node.order.customer.lastName}}
{{currency node.totalPrice node.currency}}
{{i18n "fc.sf.ui.wave.createWave.list.values.deliveryType." node.order.type}}
{{lte (arraySum node.items.edges "node.requestedQuantity") value}}
```

---

## Testing Mystique Components

OOTB testing defaults:

- use **Jest + React Testing Library** for behavioral tests
- mock Mystique hooks with `jest.spyOn(...)`
- keep package-local helpers such as `src/utils/TestUtils.tsx` or `src/utils/MystiqueMocks.ts`
- use Storybook stories for visual states and Storyshots for selective snapshot coverage
- add `src/index.test.ts` and `manifests/Manifest.test.ts` alongside component tests for registry/i18n/query validation

Best reference plugins by topic:

- **Store** — broad component coverage, hook mocks, local `TestUtils`, complex interaction tests
- **Returns** — field-heavy tests, `entityContext`, validation, user-action dialog behaviors
- **Analytics** — data-heavy component tests and focused `useData`/`useQuery` usage
- **Core** / **OMS** — Storyshots and snapshot-specific Storybook setup

### Mock Utilities for Mystique Hooks

Mystique hooks are runtime-injected by the host app. In unit tests, mock them with `jest.spyOn`:

```typescript
// src/utils/MystiqueMocks.ts — reusable mock factories
import * as UseQuery from 'mystique/hooks/useQuery';
import * as UseSettings from 'mystique/hooks/useSettings';
import * as UseI18n from 'mystique/hooks/useI18n';
import * as UseData from 'mystique/hooks/useData';
import * as UseAuth from 'mystique/hooks/useAuth';

/** Mock useQuery — set fetching/data/error state */
export const mockUseQuery = <T>(status: 'ok' | 'loading' | 'error', data?: T) => {
  jest.spyOn(UseQuery, 'useQuery').mockReturnValue([
    {
      data: status === 'ok' ? data : undefined,
      fetching: status === 'loading',
      error: status === 'error' ? { message: 'Mock error' } : undefined,
      stale: false,
    },
    jest.fn(),
  ] as any);
};

/** Mock multiple useQuery calls in order. */
export const mockUseQuerySequence = (...responses: any[]) => {
  const spy = jest.spyOn(UseQuery, 'useQuery');
  responses.forEach(response => {
    spy.mockImplementationOnce(() => response);
  });
  return spy;
};

/** Mock useSettings — set status/value per key */
export const mockUseSettings = (entries: Record<string, { status: string; value?: any }>) => {
  const result: Record<string, any> = {};
  for (const [key, { status, value }] of Object.entries(entries)) {
    result[key] = { status, result: status === 'ok' ? { value } : undefined, resultType: 'object' };
  }
  jest.spyOn(UseSettings, 'useSettings').mockReturnValue(result);
};

/** Mock useI18n — pass-through translate */
export const mockUseI18n = () => {
  jest.spyOn(UseI18n, 'useI18n').mockReturnValue({
    translate: (key: string) => key,
    translateOr: (keys: string[]) => keys[keys.length - 1],
    prefixedTranslate: (key: string) => key,
    language: { current: 'en', available: [{ label: 'English', code: 'en' }] },
  } as any);
};

/** Mock useData — set page context */
export const mockUseData = (data: any = {}, variables: any = {}) => {
  jest.spyOn(UseData, 'useData').mockReturnValue({
    context: { data, dataSource: [] },
    variables: { current: variables, available: [], setVariables: jest.fn() },
    loadingState: 'ready',
  } as any);
};

/** Mock useAuth — set user and context */
export const mockUseAuth = (overrides?: { username?: string; contextId?: string }) => {
  jest.spyOn(UseAuth, 'useAuth').mockReturnValue({
    user: { username: overrides?.username ?? 'testuser', firstName: 'Test', timezone: 'UTC', language: 'en', roles: [] },
    context: { current: { contextId: overrides?.contextId ?? '1', contextType: 'RETAILER' }, roleCheck: () => ({ hasRole: true }) },
    switchContext: jest.fn(), logout: jest.fn(),
  } as any);
};
```

### Test File Pattern

```typescript
import { render, screen } from '@testing-library/react';
import { MyComponent } from './MyComponent';
import { mockUseQuery, mockUseSettings, mockUseI18n } from '../../utils/MystiqueMocks';

beforeEach(() => { mockUseI18n(); });

describe('MyComponent', () => {
  it('renders data when query succeeds', () => {
    mockUseQuery('ok', { orderById: { ref: 'ORD-001', status: 'BOOKED' } });
    mockUseSettings({ config: { status: 'ok', value: { pageSize: 25 } } });
    render(<MyComponent data={{ ref: 'ORD-001' }} />);
    expect(screen.getByText('ORD-001')).toBeTruthy();
  });

  it('shows loading spinner while fetching', () => {
    mockUseQuery('loading');
    const { container } = render(<MyComponent />);
    expect(container.querySelector('[class*="loading"]')).toBeTruthy();
  });
});
```

**Reference:** All three Fluent reference plugins (Store, Returns, Analytics) include TestUtils with this jest.spyOn pattern. Generate `MystiqueMocks.ts` in every scaffolded project.

### Field Test Pattern

For `FieldRegistry` components, include the real dialog props that OOTB fields depend on:

```typescript
const fieldProps: FormFieldProps<string> = {
  name: 'reason',
  label: 'Reason',
  value: '',
  defaultValue: '',
  source: 'settings.returnReasons',
  entityContext: [{ entity: { id: '123', type: 'ORDER', ref: 'ORD-123' } }],
  lastSubmit: undefined,
  error: undefined,
  onChange: jest.fn(),
  onBlur: jest.fn(),
};
```

Prefer `entityContext` over URL parsing in tests and implementation. Only fall back to `window.location.hash` when extra route data such as `retailerId` is needed.

### Snapshot Policy

- Prefer behavioral assertions for most tests
- Keep `.stories.tsx` files next to components/fields for local visual review
- Use `.storybook/Storyshots.test.ts` when you want snapshot coverage
- Treat `react-test-renderer` as Storyshots plumbing, not the default component-test API
- Wrap stories/tests with `FixMuiClassnames` when stable MUI snapshot output matters

---

## Scanner / Barcode Integration

Store-app fields frequently integrate barcode scanning. The pattern combines `ComponentRegistry.get()` for hardware scanners with match/filter/toast logic:

```typescript
const handleScan = useCallback((scannedValue: string) => {
  const matches = rows.filter(row =>
    scannerFields.some(f => row[f]?.toLowerCase() === scannedValue.toLowerCase())
  );
  if (matches.length === 1) {
    dispatch({ type: 'INCREMENT', itemId: matches[0].id });
    pushToast('success', translateOr(['scan.ok', 'Scanned {{val}}'], { val: scannedValue }));
  } else if (matches.length > 1) {
    setFilteredRows(matches);  // show disambiguation UI
  } else {
    pushToast('warning', translateOr(['scan.miss', '{{val}} not found'], { val: scannedValue }));
  }
}, [rows, scannerFields]);

const Scanner = ComponentRegistry.get('fc.util.scanner.hidden');
return Scanner ? <Scanner onScan={handleScan} /> : <ManualScanInput onSubmit={handleScan} />;
```

**Reference:** Store plugin's WavePickList adds row highlighting (`{ colour: 'success', fadeDuration: 2000 }`) and scroll-to-scanned-item. Scanner fields are configurable via `useSettings`.

---

## List Row Expansion Pattern

Lists (`fc.list`, `fc.quantity.list`) support expandable rows via the `row.expansion` config. Register a content component, then reference it as the expansion.

**Step 1: Create the expansion component**
```typescript
import { FC } from 'react';
import { Container, FormControl, TextField } from '@material-ui/core';
import { MystiqueSelect } from 'mystique/components/MystiqueSelect';
import { useI18n } from 'mystique/hooks/useI18n';

interface RowExpansionProps {
  data: any;            // Row data (from dataSource: "node")
  settings: any;        // Injected via props
  dispatch: Function;   // State update callback
}

export const ItemDetailExpansion: FC<RowExpansionProps> = ({ data, settings, dispatch }) => {
  const { translateOr } = useI18n();
  return (
    <Container style={{ display: 'flex', padding: '16px 0' }}>
      <FormControl style={{ flexBasis: '25%', marginLeft: 16 }}>
        <MystiqueSelect
          options={settings.reasons.map(r => ({ label: r.label, value: r.value }))}
          label={translateOr(['acme.label.reason', 'Reason'])}
          onChange={(e) => dispatch({ type: 'update', value: { ...data.state, reason: e.target.value } })}
        />
      </FormControl>
      <TextField
        label={translateOr(['acme.label.comment', 'Comment'])}
        style={{ flexBasis: '50%', marginLeft: 16 }}
        onChange={(e) => dispatch({ type: 'update', value: { ...data.state, comment: e.target.value } })}
      />
    </Container>
  );
};
```

**Step 2: Register as content component**
```typescript
ComponentRegistry.register(['acme.content.item-detail'], ItemDetailExpansion, { category: 'content' });
```

**Step 3: Wire into list config (in your FieldRegistry field)**
```typescript
import { MystiqueComponentInstance } from 'mystique/types/manifest';

const expansionConfig: MystiqueComponentInstance = {
  component: 'acme.content.item-detail',
  props: {
    settings: settings,      // Pass from parent
    dispatch: dispatch,       // Pass reducer dispatch
    colSpan: columns.length + 1,
  },
  dataSource: 'node',        // Each row's data
};

const listProps = {
  ...fieldConfig,
  row: { expansion: expansionConfig },
  attributes: columns,
  data: items,
};
```

**Key points:**
- Expansion components receive the row's `node` data via `dataSource: "node"`
- Pass parent state/dispatch via `props` for bi-directional communication
- The expansion collapses/expands based on row selection state
- Use MUI `Collapse` with `appear={true}` for smooth animation on mount

**Reference:** Fluent Returns plugin uses `ReturnRowExpansion` (condition/reason/comment dropdowns) as a row expansion inside `ReturnItemList`.

---

## Styling Strategy

Material-UI 4 provides three CSS-in-JS approaches. Each has a correct use case:

| Approach | When | Example |
|----------|------|---------|
| `makeStyles()` | Component-scoped styles, theme access, conditional classes | Most components |
| `styled()` | Reusable MUI component variants (custom Button, Input) | Shared UI library |
| `css` prop (Emotion) | One-off style overrides on MUI components | Quick fixes, prototyping |

```typescript
// makeStyles — conditional via props
const useStyles = makeStyles<Theme, { isMobile: boolean }>((theme) => ({
  root: { padding: ({ isMobile }) => isMobile ? theme.spacing(1) : theme.spacing(2) },
}));

// styled() — reusable variant
const CompactButton = styled(Button)({ padding: '4px 8px', fontSize: '0.75rem' });

// css prop — one-off override (use sparingly)
<Accordion css={{ '&&': { boxShadow: 'none', width: '100%' } }}>
```

**MUI internal selector overrides** (e.g., `& .MuiPaper-elevation0`) are fragile — document why the override is needed.

### MystiqueThemeColours — Platform Theme Colors

The Mystique theme exposes named colors via `MystiqueThemeColours`. Use these instead of hardcoded hex values for theme consistency:

```typescript
// Available theme color names (from @types/mystique/services.d.ts):
type MystiqueThemeColours = {
  primary: string;     secondary: string;
  success: string;     error: string;
  warning: string;     info: string;
  // + 8 more palette colors
};

// Access via MUI useTheme (colors map to MUI palette):
import { useTheme } from '@material-ui/core/styles';
const theme = useTheme();
const primaryColor = theme.palette.primary.main;

// In makeStyles:
const useStyles = makeStyles((theme) => ({
  highlight: { backgroundColor: theme.palette.success.light },
}));

// In QuantityList innerText colors:
innerText: { options: { color: { all: 'success', some: 'warning', none: 'error' } } }
```

Prefer theme color names (`'success'`, `'primary'`) over hex codes in component props and list configs. The platform resolves them to the active theme's colors.

---

## Manifest Wizard Flows (`fc.page.wizard`)

Multi-step creation wizards use `fc.page.wizard` with linear step progression. Each step renders its own descendants (matched by index), and the final step typically triggers a mutation.

```json
{
  "type": "page",
  "path": "returns/create/:id/:ref",
  "fullScreen": true,
  "component": "fc.page.wizard",
  "props": {
    "title": "i18n:acme.returns.create.title",
    "steps": [
      { "label": "i18n:acme.returns.step.select" },
      { "label": "i18n:acme.returns.step.review" },
      { "label": "i18n:acme.returns.step.confirm" }
    ],
    "closePath": "returns"
  },
  "descendants": [
    { "component": "fc.list", "props": { ... } },
    { "component": "fc.card", "props": { ... } },
    { "component": "fc.card", "props": { ... } }
  ]
}
```

**Key props:** `steps` (required — array of `{ label }` objects), `closePath` (navigate on cancel/close), `fullScreen: true` (typical for creation flows). `fc.page.wizard.summary` can be added as a descendant for a sticky sidebar summary panel.

**SDK component integration:** Custom FieldRegistry fields inside wizard steps receive standard `FormFieldProps`. Use `onChange(null)` to signal "not ready" (disables Next/Submit). The wizard manages step validation — individual fields don't need to know about wizard state.

**Reference:** Returns plugin uses `fc.page.wizard` for the create-return flow with select → review → confirm steps. See `fluent-mystique-builder/components/fc.page.wizard.md` for full component spec.

---

## Connection<T> Nested Traversal

Fluent GraphQL returns paginated data in `Connection<T>` shape: `{ edges: [{ node: T, cursor? }], pageInfo? }`. Cross-entity calculations require nested flatMap traversal.

### extractEdgeNodes utility

```typescript
// src/utils/graphql.ts — reusable edge extraction
type Connection<T> = { edges?: Array<{ node: T }> } | null | undefined;

export function extractEdgeNodes<T>(connection: Connection<T>): T[] {
  return connection?.edges?.map(e => e.node) ?? [];
}
```

### Nested traversal (2+ levels deep)

```typescript
// Calculate total filled quantity for a specific order item across all fulfilments
const totalFilled = extractEdgeNodes(order?.fulfilments)
  .flatMap(f => extractEdgeNodes(f.items))
  .filter(fi => fi?.orderItem?.id === targetItemId)
  .reduce((sum, fi) => sum + (fi?.filledQuantity || 0), 0);

// Collect all article refs across all fulfilments
const articleRefs = extractEdgeNodes(order?.fulfilments)
  .flatMap(f => extractEdgeNodes(f.articles))
  .map(a => a.ref);

// Sum line items from nested fulfilment choices
const lineTotal = extractEdgeNodes(order?.fulfilmentChoices)
  .flatMap(fc => extractEdgeNodes(fc.fulfilments))
  .flatMap(f => extractEdgeNodes(f.items))
  .reduce((sum, item) => sum + (item?.requestedQuantity ?? 0), 0);
```

**Defensive patterns:**
- Always use `?.` on connection fields — they may be null if not requested in the query
- Use `?? []` / `|| 0` for accumulation — never assume array/numeric fields exist
- Prefer `extractEdgeNodes` over inline `.edges?.map(e => e.node)` — easier to read and consistent

---

## Schema Shape Gotchas

Several Fluent GraphQL types have shapes that surprise developers. Validate with `mcp graphql.validate` before embedding queries.

| Field | Expected | Actual | Impact |
|-------|----------|--------|--------|
| `Order.totalPrice` | `{ amount, currency }` | `Float` (scalar) | `totalPrice { amount }` query fails |
| `Order.totalTax` | `{ amount, currency }` | `Float` (scalar) | Same as totalPrice |
| `me.roles` | `Connection<Role>` with edges/nodes | `[{ role: { name } }]` flat array | `roles { edges { node { ... } } }` query fails |
| `OrderItem.paidPrice` | Always present | Nullable — depends on order type | Needs nullish chain: `paidPrice ?? totalPrice ?? price ?? 0` |
| `Order.currency` | Field on Order | Does NOT exist on Order | Use `OrderItem.currency` instead |
| `Network.name` | String field | Does NOT exist | Use `Network.ref` instead |
| `Product` | Single type | Union: `StandardProduct \| VariantProduct` | Must use fragment spreads |

```typescript
// WRONG — totalPrice is a Float, not an object
const total = order.totalPrice.amount;

// CORRECT
const total = order.totalPrice; // already a number

// WRONG — me.roles is a flat array
const roles = me.roles.edges.map(e => e.node.role.name);

// CORRECT
const roles = me.roles.map(r => r.role.name);

// Defensive price extraction (handles nullable fields)
const price = item.paidPrice ?? item.totalPrice ?? item.price ?? 0;
```

**Rule:** Always run `mcp graphql.validate` or `graphql.introspect` on your queries. Never assume a field's shape from the name — check the schema.

### ConvertableVariable — Non-String GraphQL Variables

Manifest template strings always produce string values, but GraphQL often requires `Int` or `ID` types. The `ConvertableVariable` pattern tells Mystique to `JSON.parse()` the value before sending it as a query variable:

```json
{
  "data": {
    "query": "query($id: ID!) { orderById(id: $id) { id ref status } }",
    "variables": {
      "id": { "value": "{{id}}", "parse": true }
    }
  }
}
```

Without `parse: true`, `{{id}}` would be sent as the string `"12345"` instead of the integer `12345`, causing type mismatch errors on `ID!` or `Int!` variables.

**When to use:** Any manifest query variable that is `ID`, `Int`, `Float`, `Boolean`, or `[Int]` type. String variables do NOT need this — they're already strings.

**Type definition** (from `@types/mystique/manifest.d.ts`):
```typescript
interface ConvertableVariable {
  value: string;    // Template expression
  parse: true;      // Always true — triggers JSON.parse()
}
```

---

## useUpdateTracer (Debug Utility)

A development-only hook that logs which props/state caused a re-render. Useful for diagnosing performance issues in complex fields.

```typescript
// src/utils/useUpdateTracer.ts — REMOVE BEFORE PRODUCTION
import { useEffect, useRef } from 'react';

export function useUpdateTracer(name: string, props: Record<string, any>): void {
  const prev = useRef(props);
  useEffect(() => {
    const changed: Record<string, { from: any; to: any }> = {};
    for (const key of Object.keys({ ...prev.current, ...props })) {
      if (prev.current[key] !== props[key]) {
        changed[key] = { from: prev.current[key], to: props[key] };
      }
    }
    if (Object.keys(changed).length > 0) {
      console.log(`[UpdateTracer] ${name}:`, changed);
    }
    prev.current = props;
  });
}

// Usage in a component:
const MyField: FC<FormFieldProps<string>> = (props) => {
  useUpdateTracer('MyField', props);
  // ... component body
};
```

**WARNING:** Remove all `useUpdateTracer` calls before production builds. Add a lint rule or build-time check: `grep -rn "useUpdateTracer" src/ && echo "FAIL: debug hook in source" && exit 1`.

---

## data-testid Convention

Add `data-testid` attributes to key interactive and container elements for E2E testing (Cypress, Playwright, Chrome DevTools MCP).

**Naming convention:** `{PluginPrefix}_{ComponentName}_{element}`

```typescript
// Root element — always add
<div data-testid="Acme_AppeasementForm_root">
  {/* Action buttons */}
  <Button data-testid="Acme_AppeasementForm_submit">Submit</Button>
  <Button data-testid="Acme_AppeasementForm_cancel">Cancel</Button>

  {/* Input fields */}
  <TextField data-testid="Acme_AppeasementForm_amount" />

  {/* Lists/tables */}
  <Table data-testid="Acme_OrderItems_table">
    <TableRow data-testid={`Acme_OrderItems_row_${item.ref}`} />
  </Table>
</div>
```

**Rules:**
- Root element: `{Prefix}_{Component}_root` — always present
- Interactive elements: buttons, inputs, selects, checkboxes
- Dynamic rows: include a unique identifier suffix (`_row_${ref}`)
- Never use `data-testid` for styling — use `className` for that
- Prefix with your plugin name to avoid collisions between plugins

**Reference:** Store plugin uses `data-testid='CreateWaveTable'`, `data-testid='Scanner_input'`. Returns plugin uses `data-testid='ReturnItemList'`.

---

## Advanced Production Patterns

### Settings-Driven Component Configuration

Components can read Fluent settings at runtime via `useSettings` to configure behavior (dropdowns, feature flags, field visibility) without redeployment.

```typescript
import { useSettings } from 'mystique/hooks/useSettings';

const MyComponent: React.FC<StdProps> = (props) => {
  const settings = useSettings({
    config: 'acme.mystique.component.config',
    options: 'acme.mystique.dropdown.options'
  });

  // Check loading state
  if (settings.config.status === 'loading') return <CircularProgress />;

  // Safe access with fallback
  const configValue = settings.config.status === 'ok'
    ? settings.config.result?.value
    : undefined;

  const dropdownOptions = settings.options.status === 'ok'
    ? settings.options.result?.value?.active || []
    : [];

  return (
    <Select>
      {dropdownOptions.map(opt => (
        <MenuItem key={opt.value} value={opt.value}>{opt.label || opt.name}</MenuItem>
      ))}
    </Select>
  );
};
```

**Settings dropdown value formats:** Two patterns exist in OOTB:
- Workflow user action dropdowns (`source: "settings.*"`): `{ "active": [{ "name": "Display", "value": "CODE" }] }` — uses `name` key, requires `active` wrapper
- SDK component dropdowns (`useSettings`): `[{ "label": "Display", "value": "CODE" }]` — uses `label` key, no wrapper needed
- **Best practice:** Use `opt.label || opt.name` to handle both formats gracefully.

**When to use**: Runtime-configurable behavior (A/B testing, multi-tenant customization, admin-managed dropdown options). Avoids redeployment for configuration changes.

**Advanced: Settings-driven field configuration (full pattern from Returns plugin)**

A single Fluent setting can control an entire field's behavior — query, columns, validation rules, scanner config. This is the gold standard for reusable, configurable form fields.

```typescript
// 1. Define the config interface
interface FieldConfig {
  query?: string;                    // Custom GraphQL query (overrides default)
  attributes?: TranslatableAttribute[];  // Column definitions
  defaultPageSize?: number;
  inputs?: {                         // Per-field validation rules
    returnReason?: { validation?: boolean };
    returnCondition?: { validation?: boolean };
    returnReasonComment?: { validation?: boolean };
  };
  scanner?: {                        // Barcode scanner config
    scannerFields?: string[];
    filterFields?: string[];
    scanTimeout?: number;
    camera?: CameraProps;
  };
  summary?: {
    descendants?: MystiqueComponentInstance[];  // Configurable summary JSX
  };
}

// 2. Fetch multiple settings in one call
const settings = useSettings({
  conditions: 'RETURN_CONDITION',           // Dropdown options
  reasons: 'RETURN_REASON',                 // Dropdown options
  fieldConfig: 'fc.mystique.fields.returns', // Full field config
});

// 3. Apply i18n translation to the config
import { recursiveValueDecorator } from 'mystique/utils';
const { prefixedTranslate } = useI18n();

const rawConfig = settings.fieldConfig.status === 'ok'
  ? settings.fieldConfig.result.value as FieldConfig
  : undefined;

// Translates all i18n-prefixed strings in nested config
const config = rawConfig
  ? recursiveValueDecorator(rawConfig, 'string', prefixedTranslate) as FieldConfig
  : undefined;

// 4. Use config to drive rendering
const query = config?.query || defaultQuery;
const columns = config?.attributes || defaultColumns;
```

**Benefits:** Retailers customize field behavior (columns, validation, scanner) by updating a Fluent setting — no code change or redeployment needed. This is how Fluent's own Returns plugin achieves per-retailer customization.

**`useSettings` vs `getSettings`**:
- `useSettings()` — React hook, reactive, auto-refetches. Use in component render.
- `getSettings()` — Promise-based, imperative. Use in event handlers or async flows.

```typescript
// Imperative (in onClick handler)
import { getSettings } from 'mystique/hooks/useSettings';
const handleClick = async () => {
  const result = await getSettings({ myKey: 'setting.name' });
  // use result.myKey.result?.value
};
```

### StylesProvider — CSS Scoping for Multi-Plugin Environments

When multiple SDK plugins load on the same page, MUI class names can collide. Use `StylesProvider` with a custom class name generator to scope styles. (See also the CSS Class Name Scoping section above for the basic pattern.)

The key production pattern is wrapping component registrations directly, ensuring every instance is scoped even when rendered by the Mystique host:

```typescript
import { StylesProvider, createGenerateClassName } from '@material-ui/core/styles';

const generateClassName = createGenerateClassName({
  productionPrefix: 'acme',  // Your namespace prefix
  seed: 'acme-plugin',       // Additional uniqueness seed
});

// Wrap your component tree
const MyPluginWrapper: React.FC = ({ children }) => (
  <StylesProvider generateClassName={generateClassName}>
    {children}
  </StylesProvider>
);

// In index.tsx registration:
ComponentRegistry.register(
  ['acme.content.my-component'],
  (props) => (
    <StylesProvider generateClassName={generateClassName}>
      <MyComponent {...props} />
    </StylesProvider>
  ),
  { category: 'content' }
);
```

**When required**: Always use when your plugin shares a page with other SDK plugins or OOTB components. The `productionPrefix` prevents class name collisions in production builds.

### FormFieldProps — entityContext and Advanced onChange

Field components receive more than just `value` and `onChange`. The full `FormFieldProps<T>` interface includes entity context and rich onChange options. (See the Form Field Development section above for the complete interface definition and detailed onChange API.)

This section provides a quick-reference summary of the advanced props:

```typescript
interface FormFieldProps<T = any> {
  name: string;
  label?: string;
  value?: { value: T };
  disabled?: boolean;
  error?: string;

  // Entity context — array of entities the form is editing
  entityContext?: Array<{
    entity: { id: string; ref: string; type: string; status: string; attributes?: any[] };
    entityType: string;
  }>;

  // Last form submission timestamp (useful for duplicate detection)
  lastSubmit?: number;

  // onChange with optional metadata
  onChange?: (value: T, options?: OnChangeOptions) => void;
  onBlur?: () => void;
}

interface OnChangeOptions {
  confirm?: boolean;           // Show confirmation dialog before submitting
  summary?: string;            // Summary text shown in confirmation
  disableActionButton?: boolean; // Disable form submit button (validation gate)
  isInvalid?: boolean;         // Mark field as invalid
}
```

**entityContext usage** — Access parent entity data from a field component:
```typescript
const MyField: React.FC<FormFieldProps<string>> = ({ entityContext, onChange }) => {
  const entityId = entityContext?.[0]?.entity?.id;
  const entityType = entityContext?.[0]?.entityType;

  // Use entity ID to fetch related data
  const [result] = useQuery(RELATED_QUERY, { id: entityId });
  // ...
};
```

**Disabling the action button from field validation:**
```typescript
const validate = (value: string) => {
  if (!value || value.length < 3) {
    onChange(value, { disableActionButton: true, isInvalid: true });
  } else {
    onChange(value, { disableActionButton: false });
  }
};
```

### getQuery vs useQuery — Decision Framework

| Pattern | API | When to Use |
|---------|-----|-------------|
| **Reactive** | `useQuery(query, vars)` | Component needs data on mount and re-renders on data change. Returns `[result, refetch]`. |
| **Imperative** | `getQuery(query, vars)` | One-shot fetch in event handler, form submit, or async flow. Returns `Promise<data>`. |

```typescript
// Reactive — data drives render
const [ordersResult, refetchOrders] = useQuery<OrdersResult>(ORDERS_QUERY, { first: 10 });
if (ordersResult.fetching) return <Loading />;
return <OrderList orders={ordersResult.data?.orders} />;

// Imperative — mutation in handler
const handleCreateOrder = async () => {
  const result = await getQuery<CreateOrderResult>(CREATE_ORDER_MUTATION, { input: orderData });
  if (result.data?.createOrder?.id) {
    window.location.hash = `#/orders/${result.data.createOrder.id}`;
  }
};
```

**Rules:**
- Never call `getQuery` inside render (use `useQuery` instead)
- Never use `useQuery` for mutations (use `getQuery` in event handlers)
- `useQuery` auto-caches by query+variables; `getQuery` does not cache
- Both share the same authentication context from `useAuth`

### Multi-Entry-Point Bundle Strategy

Production projects often need multiple bundles from the same codebase for different deployment targets (e.g., OMS App vs Store App, or a minimal bundle for specific pages).

```
src/
├── index.tsx                    # Default entry (all components)
├── entries/
│   ├── logout-only.tsx          # Minimal: just LogoutMonitor
│   ├── user-management.tsx      # Feature: UserManagement + deps
│   └── store-fields.tsx         # Store app: FieldRegistry only
```

Each entry file registers a subset of components:
```typescript
// entries/logout-only.tsx
import { ComponentRegistry } from 'mystique/registry/ComponentRegistry';
import LogoutMonitor from '../components/LogoutMonitor';
ComponentRegistry.register(['acme.content.logout-monitor'], LogoutMonitor, { category: 'content' });
```

Webpack config selects entry at build time:
```typescript
// webpack.config.ts
entry: process.env.BUNDLE_TARGET
  ? `./src/entries/${process.env.BUNDLE_TARGET}.tsx`
  : './src/index.tsx',
```

**When to use**: When different apps (OMS App vs Store App) need different component subsets, or when bundle size matters for specific pages. Each entry produces a separate `bundle.[hash].js` that is referenced as a distinct plugin in the target app's root manifest `plugins[]`.

---

## Key Best Practices

- **Wrap `onChange` in `useEffect`** — Direct calls in render cause infinite loops
- **Use `<DynamicValue>` for configurable output** — Keeps components reusable
- **Use `<Children />` for composable containers** — Manifest-driven nesting
- **Type everything** — Avoid `any`; use `FormFieldProps<T>` for fields
- **Handle loading and error states** — Every `useQuery` needs loading spinner + error display
- **Use local `./` imports** — Imports without `./` prefix fail in webpack externals
- **React 17 is the baseline** — Newer SDK versions (v1.2024.9+) support React 18. Check `sdk-version.json` before using React 18 APIs (`useId`, `useSyncExternalStore`, `useTransition`, `createRoot`)
- **Scope CSS class names** — Use `StylesProvider` + `createGenerateClassName` with a unique prefix
- **Use `useReducer` for complex fields** — 3+ interrelated state values, undo, aggregate validation
- **Prefer `entityContext` over URL parsing** — Framework-provided entity identity in FieldRegistry fields
- **Guard `ComponentRegistry.get()` calls** — Platform components may not exist in all environments
- **Settings-driven configuration** — Use `useSettings` + `lodash.merge` over defaults for runtime overrides
- **Mock Mystique hooks in tests** — Use jest.spyOn with MystiqueMocks.ts (see Testing section)
- **Wrap scanner integration with fallback** — Use `ComponentRegistry.get()` + manual input fallback
- **Use `extractEdgeNodes` for Connection traversal** — Consistent, safe nested edge extraction
- **Validate schema shapes** — `totalPrice` is Float, `me.roles` is flat array, `Product` is a union type
- **Add `data-testid` to interactive elements** — `{Prefix}_{Component}_{element}` convention for E2E
- **Remove `useUpdateTracer` before production** — Debug-only hook, add build-time check
