---
name: fluent-mystique-sdk-reference
description: Mystique Component SDK API reference — React hooks, composition primitives, field patterns, and Material-UI constraints. Read-only lookup for building custom Fluent Commerce UI components. Triggers on "mystique hooks", "sdk reference", "component api", "field registry", "mystique sdk".
user-invocable: true
allowed-tools: Read, Glob, Grep
argument-hint: [hook-name|pattern-name]
cursor-tier: manual
---

# Mystique Component SDK Reference

Read-only API reference for the Fluent Commerce OMX Component SDK. Use this when building or extending custom Mystique components and you need to look up hook signatures, composition patterns, field registration, or platform constraints.

**Related skills:**
- `/fluent-mystique-scaffold` — Create a new SDK component project from scratch
- `/fluent-mystique-component` — Add components to an existing SDK project
- `/fluent-mystique-builder` — Build and edit manifest JSON that references custom components

## SDK Overview

The OMX Component SDK is a React/TypeScript project template that produces a `bundle.[hash].js` loadable by the Fluent OMX host application. The host provides React, ReactDOM, Material-UI, and Mystique globals as webpack externals — your bundle does NOT include these.

**Download URL (always latest):** `https://fluent.apps.fluentcommerce.com/_sdk/latest/omx-component-sdk.zip`

**Source layout convention:**

```text
accounts/<PROFILE>/SOURCE/
├── backend/   # Java/module/plugin source
└── frontend/  # OMX SDK projects (Mystique)
```

**Bundle delivery model:** `yarn build` produces `dist/bundle.[hash].js` and `dist/version.json`. Deploy the whole `dist/` folder and set manifest `plugins[].src` to the plugin folder URL (trailing `/` recommended), so Mystique resolves `version.json` to the latest hashed bundle.

**Three registries — pick the right one:**

| 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)` |

## @types/mystique/ Type Definitions

The `@types/mystique/` directory contains 7 TypeScript declaration files that define the Mystique SDK API surface.

| File | Purpose |
|------|---------|
| `index.d.ts` | Common types: `TranslatableString`, `QueryResponse<T>`, `Connection<T>`, `Edge<T>` |
| `manifest.d.ts` | Full manifest v2.0 schema: `MystiqueManifest`, `MystiqueSection`, `MystiquePage`, `MystiqueComponentInstance`, `MystiquePlugin`, `MystiqueManifestFragment`, `GQLQuery` |
| `components.d.ts` | Component prop interfaces: `Card`, `CardContent`, `Children`, `DynamicValue`, `Loading`, `List`, `QuantityList`, `QuantitySelectorComponent`, `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 for theming) |
| `registry.d.ts` | Registry interfaces: `ComponentRegistry` (register/get components), `FieldRegistry` (register/get form fields), `TemplateRegistry` (register/render Handlebars helpers) |
| `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.** They provide full IntelliSense in the IDE for all Mystique hooks, components, and registries. The actual implementations are provided at runtime by the host app via `window.Mystique`.

---

## React Hooks Usage Patterns

The Mystique SDK provides hooks via `import { useAuth, useQuery, ... } from 'mystique/hooks'`. All hooks are provided by the host app at runtime.

### useAuth — Authentication and Context

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

const MyComponent = () => {
  const auth = useAuth();
  // auth.username: string — current user's login name
  // auth.firstName, auth.lastName: string
  // auth.timezone: string — user's timezone (e.g., 'Australia/Sydney')
  // auth.language: string — user's locale (e.g., 'en')
  // auth.roles: Array<{ name: string }> — assigned roles
  // auth.contextId: number — current retailer/location context ID
  // auth.contextType: string — 'RETAILER' | 'LOCATION'
  // auth.switchContext(contextId, contextType) — switch retailer/location
  // auth.logout() — end session

  const isAdmin = auth.roles.some(r => r.name === 'ADMIN');
  return isAdmin ? <AdminPanel /> : <StandardView />;
};
```

### useQuery — GraphQL Data Fetching

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

// Returns [UseQueryState<T>, reexecuteFn]
// UseQueryState = { fetching: boolean; stale: boolean; data?: T; error?: CombinedError }
const OrderDetail = ({ orderId }: { orderId: string }) => {
  const [result] = useQuery<OrderByIdResponse>(
    `query OrderById($id: ID!) { orderById(id: $id) { id ref status type createdOn } }`,
    { id: orderId }
  );
  const { fetching, data, error } = result;

  if (fetching) return <Loading />;
  if (error) return <div>Error: {error.message}</div>;

  const order = data?.orderById;
  // Connection/pagination pattern:
  // data.orders.edges[].node — array of entities
  // data.orders.edges[].cursor — cursor for pagination (on each edge, not pageInfo)
  // data.orders.pageInfo.hasNextPage — boolean
  return <div>{order.ref} — {order.status}</div>;
};
```

### useSettings — Hierarchical Settings Access

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

const ConfiguredComponent = () => {
  const settings = useSettings({
    enabled: 'fc.myFeature.enabled',
    maxItems: 'fc.myFeature.maxItems',
  });
  // Settings cascade: Location > Retailer > Account > Global (most specific wins)
  // Each requested alias returns: { status: 'loading' | 'error' | 'ok', result?: { value } }

  const featureEnabled = settings.enabled;
  if (featureEnabled.status === 'loading') return <Loading />;
  const enabled = featureEnabled.result?.value === 'true';
  return enabled ? <FeatureUI /> : null;
};
```

### useI18n — Internationalization

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

const TranslatedComponent = () => {
  const { translate, translateOr } = useI18n();

  // translate(key) — single key lookup, returns key if not found
  const title = translate('fc.om.orders.title');

  // translateOr([key1, key2, ..., fallback]) — try keys in order, last is fallback
  const label = translateOr(['fc.custom.order.label', 'fc.om.orders.label', 'Orders']);

  return <h1>{title}</h1>;
};
```

### useData — Page Query Data Access

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

const PageComponent = () => {
  const { context, variables, loadingState } = useData();
  const order = context.data;
  // context.data is already scoped by the nearest page/provider dataSource
  // context.dataSource is the accumulated scoping chain
  // variables.current / variables.setVariables() expose active page query variables
  return <span>{loadingState === 'ready' ? order?.ref : 'Loading...'}</span>;
};
```

### useEnv — Environment Details

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

const DebugInfo = () => {
  const env = useEnv();
  // env.accountName: string — Fluent account name
  // env.appName: string — 'oms', 'store', 'servicepoint'
  // env.apiEndpoint: string — API base URL
  return <span>Connected to {env.accountName}</span>;
};
```

---

## Component Composition Primitives

Three essential patterns for building reusable, manifest-configurable custom components.

### DynamicValue — Manifest-Configurable Content

`DynamicValue` renders a single dynamic attribute (text, image, or component) from manifest config. Lets manifest configurers control what your component displays without code changes.

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

const OrderCard = ({ data, props }) => {
  // props.attributes comes from the manifest's attributes[] array
  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 place OOTB or custom components inside your component via `descendants`.

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

const Panel = ({ data, children }) => {
  return (
    <div className="custom-panel">
      <Children />  {/* renders all descendants from manifest */}
    </div>
  );
};

// Wrapper variant: transform data for all descendants
const FilteredPanel = ({ data }) => {
  const filteredData = data.items.filter(i => i.status === 'ACTIVE');
  return (
    <div>
      <Children dataOverride={filteredData} />
    </div>
  );
};
```

### TemplateRegistry.render — Resolve Templates in Code

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

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

const CustomDisplay = ({ data, props }) => {
  // props.titleTemplate might be "Order {{ref}} — {{status}}" from manifest
  const resolvedTitle = TemplateRegistry.render(props.titleTemplate, data);
  return <h2>{resolvedTitle}</h2>;
};
```

### FieldRegistry.get — Reuse OOTB Form Fields

Compose existing OOTB field components inside custom composite fields:

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

const CompositeField = ({ onChange, value }) => {
  const NumberField = FieldRegistry.get('number');
  return (
    <div>
      <NumberField value={value.min} onChange={(v) => onChange({ ...value, min: v })} />
      <span> to </span>
      <NumberField value={value.max} onChange={(v) => onChange({ ...value, max: v })} />
    </div>
  );
};
```

### Form Field onChange — Avoid Infinite Loop

**CRITICAL:** When building form field components, `onChange` triggers a re-render. 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)} />;
};
```

### User Action Attribute to Field Mapping

Workflow user actions define `eventAttributes` with a `type` property. The Mystique form engine maps each type to a 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 with `FieldRegistry.register('myCustomType', MyFieldComponent)`. The form engine auto-selects them when a user action has `type: "myCustomType"`.

### 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 the event payload attribute names match what rules expect via `@EventAttribute`.

**Four implementation patterns:**
- **Pattern A (field-level):** Register custom FieldRegistry type → replaces ONE field. Standard form auto-renders the rest.
- **Pattern B (wrapper full-form, recommended for rich layout):** Use `useUserActionForm()` and render `result.form` inside your own ComponentRegistry card/layout. Framework still dispatches the event.
- **Pattern C (manual full-form):** Replace entire form and dispatch manually via `useRest()` / `getRest()`.
- **Pattern D (typed contract adapter):** A constrained, safer Pattern C: single fancy component + strict TypeScript interface for all expected event attributes + explicit payload mapping.

FieldRegistry alone cannot write multiple independent attribute keys in one shot — each field maps to one attribute slot. For "one fancy component submits many attributes", use Pattern C/D.

See `knowledge/platform/user-action-contract.md` §9 for full decision matrix and guardrails.

---

## React Hooks API Reference

The Mystique host app provides hooks via `mystique/hooks/*`. Type declarations are generated in `@types/mystique/hooks.d.ts`. There are **6 reactive hooks** (re-render on change) and **4 imperative functions** (Promise-based, for use in `useEffect`/event handlers). Plus `getApiDownload` for file downloads. Here is the runtime API for each:

### 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();
```

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

### 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
// result.data     — T | undefined
// result.error    — CombinedError | undefined
```

Results follow the Relay connection pattern: `edges[].node`, `edges[].cursor`, `pageInfo { hasNextPage }`. Note: 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.

### Settings-Driven Component Configuration

Use `useSettings()` when retailer or location behavior should change without a plugin redeploy. Request explicit aliases so the response shape is obvious in code reviews, generated READMEs, and support handoffs.

#### useSettings — Dynamic Configuration

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

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

// Result shape — ALWAYS check status before accessing result:
// settings.config.status  — 'loading' | 'ok' | 'error'
// settings.config.result?.value — the setting value (only when status === 'ok')
if (settings.config.status === 'ok') {
  const config = settings.config.result?.value;
} else if (settings.config.status === 'error') {
  // Show error fallback
}
```

**Hierarchy**: Location → Retailer → Account → Global (cascades up). A Location-level setting overrides Retailer, which overrides Account.

### useI18n — Internationalization

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

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

translate('fc.om.orders.title');                    // single key lookup
translateOr(['acme.orders.title', 'Orders']);        // key + fallback (RECOMMENDED)
translateOr(                                         // with interpolation
  ['fc.sf.ui.showing', 'Showing {{count}} items'],
  { count: 42 }
);
language.current;                                    // 'en'
```

**Production rule:** Use `translateOr` with a fallback for all user-facing strings. `translate()` returns the raw key if missing.

### useData — Page Query Access

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

const { context, variables, loadingState, query } = useData();

context.data;              // page/provider data scoped by the current dataSource
context.dataSource;        // accumulated scoping chain from ancestors + this component
variables.current.orderId; // active query variable values
query?.data;               // optional raw useQuery state for the current page query
loadingState;              // 'initial' | 'updating' | 'ready'
```

**Use case**: Components that consume already-fetched page/provider data or need query-variable control. Prefer `props.data` for simple rendering; reach for `useData()` when you also need `variables`, `loadingState`, or the full scoped context.

### 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
```

### getQuery — Imperative GraphQL Fetching

Promise-based alternative to `useQuery`. Use inside `useEffect` or event handlers when you need to fetch data conditionally, after another async operation, or in a loop.

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

// Inside useEffect or event handler (NEVER in render body)
useEffect(() => {
  const fetchData = async () => {
    const { data, error } = await getQuery<InventoryResponse>(
      inventoryQuery,
      { productRef: [productRef], catalogue: { ref: catalogueRef }, first: 100 }
    );
    if (error) { console.error(error); return; }
    setInventory(data?.inventoryPositions?.edges?.map(e => e.node) || []);
  };
  fetchData();
}, [productRef, catalogueRef]);
```

**When to use `getQuery` vs `useQuery`:**
- `useQuery` — reactive, re-renders on data change, good for primary component data
- `getQuery` — imperative, returns a Promise, good for conditional/sequential fetches, cursor pagination loops, validation checks

### getRest — Imperative REST API Calls

Promise-based REST call against the Fluent API. Auth headers are added automatically. Endpoint starts after the domain.

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

// Event dispatch — trigger a workflow event from custom UI
const response = await getRest('api/v4.1/event/sync', 'POST', {
  name: 'FullOrderCancel',
  accountId: accountId,
  retailerId: retailerId,
  entityType: 'ORDER',
  entityId: orderId,
  entityRef: orderRef,
  rootEntityId: orderId,
  rootEntityRef: orderRef,
  attributes: { cancellationReason: reason },
});

if (response.eventStatus?.toUpperCase() === 'COMPLETE') {
  // success — reload or navigate
}

// Job/Batch API — bulk operations
const jobResponse = await getRest('api/v4.1/job', 'POST', {
  retailerId: retailerId,
  type: 'INVENTORY_UPLOAD',
});
const jobId = jobResponse.id;
await getRest(`api/v4.1/job/${jobId}/batch`, 'POST', batchPayload);
```

**Key REST endpoints:**
| Endpoint | Method | Use |
|----------|--------|-----|
| `api/v4.1/event/sync` | POST | Fire workflow event (synchronous, waits for completion) |
| `api/v4.1/event/async` | POST | Fire workflow event (asynchronous, returns immediately) |
| `api/v4.1/job` | POST | Create a batch job |
| `api/v4.1/job/{id}/batch` | POST | Send batch payload to a job |

### useRest — Hook-Based REST API Calls

Reactive hook alternative to `getRest`. Re-renders on response.

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

const result = useRest<ResponseType>('api/v4.1/some/endpoint', 'GET');
// result.status — 'loading' | 'ok' | 'error'
// result.result — response body when status is 'ok'
```

### getSettings — Imperative Settings Retrieval

Promise-based `useSettings` alternative. Supports explicit retailer/location scoping.

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

// Fetch settings with explicit retailer scope
const settings = await getSettings(
  { reasons: 'cancellation.reasons', config: 'order.display.config' },
  retailerId,   // optional — scope to specific retailer
  locationId    // optional — scope to specific location
);
```

**Use when:** setting names are generated dynamically at runtime, or you need explicit retailer/location scoping that `useSettings` doesn't provide.

### useUserActions — Available Workflow Actions

Retrieves available user actions for the current entity based on its workflow status.

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

const actions = useUserActions();
// actions.status — 'loading' | 'ok' | 'error'
// actions.result.entity — { entityId, entityRef, type, status, ... }
// actions.result.userActions — [{ eventName, type, label, confirm? }]

// Optionally target a specific entity in the page query:
const nested = useUserActions('$.orderById.fulfilments.edges[0].node');
```

**Use case:** Custom action button bars that show/hide buttons based on available workflow transitions. Combine with `useUserActionForm` for forms.

### useUserActionForm — Generate User Action Forms

Generates a complete form for a workflow user action, including field rendering and event dispatch.

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

const action = useUserActionForm('UpdateStatus', {
  path: '$.orderById',                     // optional JSONPath target
  noSuccessMessage: false,                  // suppress default toast
  extension: {
    postSubmit: { type: 'navigate', link: '#/orders' },  // navigate after submit
    // OR: postSubmit: { type: 'callback', fn: () => refetch() }
  },
  overrides: {
    reason: { defaultValue: 'Customer request' },  // field prop overrides
  },
});

// action.status — 'loading' | 'ok' | 'submitting' | 'error'
// action.result.label — user action label (may be i18n key)
// action.result.hasVisibleFields — false if no fields or all have values
// action.result.form — ReactElement to render
// action.result.submit — function to trigger submission

return (
  <Card title={action.result?.label}>
    {action.result?.form}
    <Button onClick={action.result?.submit}>Submit</Button>
  </Card>
);
```

**Four patterns for user action UI:**
- **Pattern A (field-level):** Register custom FieldRegistry type → replaces ONE field
- **Pattern B (wrapper, recommended):** Use `useUserActionForm()` + render `result.form` inside your own layout
- **Pattern C (manual):** Replace entire form + dispatch via `getRest('api/v4.1/event/sync')`
- **Pattern D (typed adapter):** Pattern C with strict TypeScript event payload interface

### getApiDownload — File Downloads

Download files from Fluent API endpoints (e.g., consignment labels, reports).

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

const blob = await getApiDownload('api/v4/consignment/1/labelStream');
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
```

---

## Production Component Patterns

Real-world patterns from production Mystique SDK implementations. These examples use the **bundled v5 path** (`@mui/material` in project dependencies) for `sx` prop and v5 components. If your project uses v4-only, replace `sx` with `makeStyles`/`className` equivalents.

### Expandable Master-Detail Table

Common OMS pattern: parent rows (e.g., fulfilments) expand to show child tables (e.g., items, articles).

```typescript
const [expandedIds, setExpandedIds] = useState<string[]>([]);

const toggleExpand = (id: string) => {
  setExpandedIds(prev =>
    prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]
  );
};

return (
  <Table>
    <TableBody>
      {fulfilments.map(f => (
        <>
          {/* Parent row */}
          <TableRow key={f.id}>
            <TableCell>{f.ref}</TableCell>
            <TableCell>{f.status}</TableCell>
            <TableCell>
              <IconButton size="small" onClick={() => toggleExpand(f.id)}>
                {expandedIds.includes(f.id) ? <KeyboardArrowUp /> : <KeyboardArrowDown />}
              </IconButton>
            </TableCell>
          </TableRow>

          {/* Child detail (shown when expanded) */}
          {expandedIds.includes(f.id) && (
            <TableRow>
              <TableCell colSpan={3} sx={{ p: 0, backgroundColor: '#E9E9E9' }}>
                <Box sx={{ m: 2 }}>
                  <Table>
                    {/* Nested items table */}
                  </Table>
                </Box>
              </TableCell>
            </TableRow>
          )}
        </>
      ))}
    </TableBody>
  </Table>
);
```

### Custom Action Button Bar

Inject status-aware action buttons into an entity detail page:

```typescript
const OrderActionButtons = (props: StdProps) => {
  const status = props?.data?.status;
  const fulfilments = props?.data?.fulfilments?.edges || [];

  const canCancel = fulfilments.every(f => CANCELABLE_STATUSES.includes(f.node?.status));
  const canRefund = REFUNDABLE_STATUSES.includes(status);

  return (
    <Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end' }}>
      {canCancel && <CancelOrderAction {...props} />}
      <AmendAddressAction {...props} />
      {canRefund && <PartialRefundAction {...props} />}
    </Box>
  );
};

ComponentRegistry.register(['acme.content.order-actions'], OrderActionButtons, { category: 'content' });
```

### Drawer-Based Action Panel

Use a right-anchored `Drawer` instead of `Dialog` for complex multi-step operations with tables or forms:

```typescript
<Drawer anchor="right" open={open} onClose={handleClose}
  slotProps={{ paper: { sx: { width: { xs: '100%', sm: '80%', md: '60%' } } } }}
>
  <Box sx={{ display: 'flex', alignItems: 'center', p: 2 }}>
    <IconButton onClick={handleClose}><CloseIcon /></IconButton>
    <Typography variant="h6" sx={{ flexGrow: 1 }}>Cancel Order</Typography>
    <Button variant="contained" onClick={handleSubmit} disabled={isSubmitting}>Submit</Button>
  </Box>
  <Divider />
  <Box sx={{ p: 3, flex: 1, overflowY: 'auto' }}>
    {/* Multi-item table with per-row reason/qty controls */}
  </Box>
</Drawer>
```

**When Drawer vs Dialog:** Use Drawer when the operation involves tables, multi-item selection, or needs more screen space. Use Dialog for simple confirmations and small forms.

### Event Dispatch from Custom Components

Fire workflow events directly via REST instead of relying on the form engine:

```typescript
const handleSubmit = async () => {
  setIsSubmitting(true);
  try {
    const response = await getRest('api/v4.1/event/sync', 'POST', {
      name: 'CancelOrderItems',
      accountId,
      retailerId: data.retailer.id,
      entityType: 'ORDER',
      entityId: data.id,
      entityRef: data.ref,
      rootEntityType: 'ORDER',
      rootEntityId: data.id,
      rootEntityRef: data.ref,
      attributes: {
        items: selectedItems.map(item => ({
          orderItemRef: item.ref,
          productRef: item.sku,
          quantity: item.qty,
          reason: item.reason,
        })),
      },
    });

    if (response.eventStatus?.toUpperCase() === 'COMPLETE') {
      showSuccess('Operation completed');
      setTimeout(() => location.reload(), 1000);
    }
  } catch {
    showError('Something went wrong');
  } finally {
    setIsSubmitting(false);
  }
};
```

**Required event payload fields:** `name`, `entityType`, `entityId`, `retailerId`. Other fields (`accountId`, `rootEntityType`, `rootEntityId`, `rootEntityRef`, `attributes`) depend on the workflow.

### Cursor-Based Auto-Pagination

Accumulate all pages of a Relay connection using reactive `useQuery`:

```typescript
const [allItems, setAllItems] = useState<Item[]>([]);
const [cursor, setCursor] = useState<string | undefined>(undefined);

const [result] = useQuery<ResponseType>(query, { first: 200, after: cursor });

useEffect(() => {
  const edges = result?.data?.items?.edges;
  if (!edges) return;

  // Deduplicate and accumulate
  setAllItems(prev => {
    const existing = new Set(prev.map(i => i.ref));
    const newItems = edges.map(e => e.node).filter(n => !existing.has(n.ref));
    return newItems.length > 0 ? [...prev, ...newItems] : prev;
  });

  // Auto-paginate if more pages exist
  if (result.data.items.pageInfo.hasNextPage && edges.length > 0) {
    setCursor(edges[edges.length - 1].cursor);
  }
}, [result]);
```

### Settings-Driven Dropdown Values

Make dropdown options retailer-configurable via Fluent settings:

```typescript
const settings = useSettings({ reasons: 'cancellation.reasons' });
const reasons = settings.reasons?.result?.value; // array of { label, value }

<Select value={reason} onChange={e => setReason(e.target.value)}>
  {reasons?.map((r: { label: string; value: string }) => (
    <MenuItem key={r.value} value={r.value}>{r.label}</MenuItem>
  ))}
</Select>
```

### Cross-Entity Navigation via Hash

Navigate between entity detail pages using Mystique's hash-based routing:

```typescript
// Navigate to another order
window.location.hash = `#/orders/${orderId}/${retailerId}/${orderRef}`;

// Navigate to a fulfilment
window.location.hash = `#/fulfilment/${fulfilmentId}`;

// Navigate to a variant product
window.location.hash = `#/variantProduct/${catalogueRef}/${productRef}`;

// Render as a clickable link
<Button onClick={() => window.location.hash = `#/orders/${orderId}`}
  sx={{ color: '#00A9E0', textTransform: 'none', p: 0 }}>
  {orderRef}
</Button>
```

**Route patterns:** The hash path follows the manifest route configuration. Common patterns: `#/{entityType}/{id}`, `#/{entityType}/{id}/{retailerId}/{ref}`.

---

## Advanced Component Patterns

### DynamicValue — User-Configurable Display

`<DynamicValue />` renders manifest-configured attributes as text, image, or embedded component. This is the key to making components reusable across different manifest contexts:

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

// GOOD — configurer controls what is displayed
export const MyComponent: FC<ComponentProps> = ({ attribute, data }) => {
  return <DynamicValue attribute={attribute} context={data} defaultValue={<span>-</span>} />;
};

// AVOID — hardcoded field access, not reusable
export const MyComponent: FC<ComponentProps> = ({ data }) => {
  return <div>{data.order.ref}</div>;
};
```

### Children — Manifest-Driven Composition

`<Children />` renders whatever descendant components are configured in the manifest. This lets configurers compose UI without touching code:

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

// Render descendants from manifest
export const MyAccordion: FC = () => (
  <CollapsibleDiv>
    <Children />
  </CollapsibleDiv>
);

// Override data context for all descendants
export const MyDataWrapper: FC = ({ data }) => {
  const transformed = transformData(data);
  return (
    <div>
      <Children dataOverride={transformed} />
    </div>
  );
};
```

**Runtime contract:**
- Do not replace this with normal React `{children}`. Mystique uses `<Children />` to apply role checks, resolve manifest props, and rebind the descendant data context.
- Each descendant can declare its own `dataSource`; Mystique resolves that path against the current context and passes the scoped result to the child as `data`.
- `dataOverride` replaces the current data context for descendants. `descendantsOverride` replaces which manifest descendants render.
- `DynamicValue` component-type attributes use the same mechanism under the hood: they render a descendant component through `<Children descendantsOverride={[...]} dataOverride={context} />`.

### TemplateRegistry.render — Resolve Manifest Templates in Code

When your component receives a template string from the manifest (e.g., `"{{order.ref}} - {{order.status}}"`), resolve it at runtime:

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

const resolved = TemplateRegistry.render('{{order.ref}} - {{order.status}}', 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 your component's render:
<QuantityField name="qty" label="Quantity" value={qty} onChange={setQty} />
```

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

Form field `onChange` MUST be wrapped in `useEffect`. Calling `onChange` directly in a handler causes infinite re-renders:

```typescript
// CORRECT — useEffect breaks the render cycle
const [value, setValue] = useState('');
useEffect(() => {
  onChange({ value });
}, [value]);

// WRONG — causes infinite loop
const handleChange = (v: string) => {
  setValue(v);
  onChange({ value: v }); // triggers parent re-render → this component re-renders → calls onChange again
};
```

### User Action Attribute ↔ Field Mapping

Workflow user actions define form attributes with a `type` field. Mystique auto-selects the matching FieldRegistry entry:

| Workflow Attribute Type | FieldRegistry Key | Component |
|------------------------|-------------------|-----------|
| `STRING` | `text` | Text input |
| `INTEGER` | `number` | Number input |
| `BOOLEAN` | `boolean` | Checkbox |
| `JSON` | `json` | JSON editor |
| `<custom>` | `<custom>` | Your registered field |

Register a custom field to handle custom types: `FieldRegistry.register(['stockReservation'], StockReservationField)`.

### FieldRegistry Data Access in User Action Dialogs

User action dialogs render outside the page/provider `DataContext` chain. In custom `FieldRegistry` components, treat `props.data` and `useData()` as empty context.

- Use `entityContext?.[0]?.entity?.id` or `.ref` first when the field needs parent entity identity.
- Use `useQuery()` with a self-contained GraphQL query for extra entity reads.
- Fall back to parsing `window.location.hash` only when the host surface does not provide `entityContext`.
- Validate inline GraphQL with MCP `graphql.validate` before shipping.

### Composing Fields from Registry

Custom field components can reuse existing registered fields via `FieldRegistry.get()`. This is useful for complex-object fields where primitive parts use standard inputs:

```typescript
import { FC, useState, useEffect } from 'react';
import { FieldRegistry } from 'mystique/registry/FieldRegistry';
import { FormFieldProps } 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);

  // Reuse the standard 'number' field from the registry
  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>
  );
};
```

This avoids re-implementing standard field behaviors and ensures visual consistency with platform fields.

### ComponentRegistry.get — Runtime Component Lookup

Use `ComponentRegistry.get()` when a custom component or field should reuse an existing Mystique widget instead of cloning the host UX from scratch.

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

const QuantityList = ComponentRegistry.get('fc.quantity.list');
const HiddenScanner = ComponentRegistry.get('fc.util.scanner.hidden');
```

This is the preferred pattern for quantity pickers, scanner surfaces, and other OOTB building blocks that already encode platform behavior.

### Scanner / Barcode Integration

Scanner-enabled flows should compose the platform scanner component with a manual input fallback:

1. Treat scanner support as optional and always render a manual entry path.
2. Drive match fields and labels from `useSettings()` so retailer behavior stays configurable.
3. Handle single-match, multi-match, and no-match outcomes explicitly.
4. Use `pushToast()` plus `translateOr()` for scan feedback.

### Material-UI Version Lock

The host OMX app ships **Material-UI v4.12.4** externalized as `window.MaterialUI`. This is the default styling path — import from `@material-ui/core` at zero bundle cost. MUI v5/v6 APIs (`sx` prop, Emotion-based `styled()`) are NOT available via the host. To use them, you must bundle `@mui/material` as a project dependency (see the **Bundled v5 path** under Material-UI Compatibility below).

### Theme Inheritance Contract

The Mystique shell wraps the entire app in `FluentThemeProvider` (a standard MUI `ThemeProvider` around a `defaultTheme` object). Custom components rendered inside the app tree inherit this theme automatically — **no additional setup needed**.

**Do NOT create your own `ThemeProvider`** inside a custom component. The shell already provides it.

#### Theme Palette

Standard MUI palette plus a Fluent-specific `palette.mystique.colours` extension:

| Token | Value | Access |
|-------|-------|--------|
| Primary | `#00A9E0` (cyan) | `theme.palette.primary.main` |
| Primary dark | `#027ECE` | `theme.palette.primary.dark` |
| Secondary | `#26005C` (purple) | `theme.palette.secondary.main` |
| Secondary variant | `#552AA8` | `theme.palette.secondary.dark` |
| Success | `#4CAF50` | `theme.palette.success?.main` |
| Error | `#D23B3B` | `theme.palette.error.main` |
| Warning | `#EF8F00` | `theme.palette.warning?.main` |
| Border (Fluent custom) | `#D4D6D8` | `theme.palette.mystique.colours.structure.border` |
| White (Fluent custom) | `#FFFFFF` | `theme.palette.mystique.colours.constants.white` |

**Always prefer theme tokens over hardcoded hex values.** Some first-party components still hardcode colors — do not follow that pattern; use tokens for maintainability.

Some component props are typed as `keyof MystiqueThemeColours | string` — prefer named theme colours over raw hex when these props are available.

#### Typography

Font family: **Lato, sans-serif**. Body text: 14px / 400 weight / 1.6 line-height.

#### Breakpoints (extended)

| Name | Value | Note |
|------|-------|------|
| `xxs` | 0 | Fluent custom |
| `xs` | 440px | Fluent custom (differs from MUI default 0) |
| `sm` | 600px | Tablet |
| `md` | 960px | Desktop |
| `lg` | 1280px | |
| `xl` | 1920px | |

#### Theme Consumption Patterns (MUI v4 APIs only)

**Pattern 1 — `makeStyles()` (primary pattern, most common in codebase):**

```typescript
import { makeStyles, Theme } from '@material-ui/core';

const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
  root: {
    color: theme.palette.primary.main,
    padding: theme.spacing(2),
  },
  container: {
    borderTop: `1px solid ${theme.palette.divider}`,
    [theme.breakpoints.down('xs')]: {
      flexDirection: 'column',
    },
  },
  highlight: {
    borderTop: (props: StyleProps) =>
      props.highlight ? `7px solid ${props.highlight}` : 'none',
  },
}));

// Usage:
const classes = useStyles({ highlight });
return <div className={classes.root}>...</div>;
```

**Pattern 2 — `useTheme()` (dynamic access in component logic):**

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

const { spacing, palette, breakpoints } = useTheme();
const isMobile = useMediaQuery(breakpoints.down('sm'));
```

**Pattern 3 — `withStyles()` (HOC for overriding MUI component defaults):**

```typescript
import { withStyles } from '@material-ui/core';
import MuiDialogTitle from '@material-ui/core/DialogTitle';

const DialogTitle = withStyles(() => ({
  root: { padding: 0, fontSize: '14px', fontWeight: 'bold' },
}))(MuiDialogTitle);
```

**Pattern 4 — `styled()` (MUI v4 `styled`, NOT Emotion):**

```typescript
import { styled } from '@material-ui/core/styles';

const MyButton = styled(Button)(({ theme }) => ({
  [theme.breakpoints.up('md')]: { height: '30px' },
  [theme.breakpoints.down('sm')]: { height: '60px' },
}));
```

#### Theme Utility Functions

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

background: alpha(theme.palette.primary.main, 0.12);
border: `1px solid ${lighten(theme.palette.primary.main, 0.7)}`;
```

#### Spacing

Standard MUI 8px base unit: `theme.spacing(1)` = 8px, `theme.spacing(2)` = 16px.

#### Styling Decision Framework

Use this decision tree when building a new component or modifying an existing one:

```
What kind of component are you building?
│
├── Composing Mystique primitives (Card, List, Children, etc.)?
│   └── DO NOTHING — theme inherited automatically.
│       Only add styling if you need layout adjustments (flex, grid, margin).
│
├── Custom UI with standard Fluent look?
│   └── Use `makeStyles((theme) => ({...}))` — the primary pattern.
│       Access palette, spacing, breakpoints from theme callback.
│       Split into `<Name>.styles.ts` when styles exceed ~30 lines.
│
├── Dynamic styling based on props or state?
│   └── Use `makeStyles` with props: `makeStyles<Theme, StyleProps>((theme) => ({...}))`
│       OR `useTheme()` for one-off values in JSX.
│       Prefer makeStyles for repeated patterns, useTheme for conditional logic.
│
├── Overriding a default MUI component (Button, Dialog, etc.)?
│   └── Use `withStyles()` HOC to override MUI's default classes.
│       Target specific MUI class names (root, label, contained, etc.).
│
├── Responsive layout?
│   └── CSS breakpoints in makeStyles: `[theme.breakpoints.down('sm')]: {...}`
│       JS breakpoints for conditional rendering: `useMediaQuery(theme.breakpoints.down('sm'))`
│
└── Totally custom visual (chart, canvas, SVG)?
    └── Own styles OK, but still read theme for colors/spacing to stay coherent.
        Wrap in StylesProvider if using any MUI styling APIs.
```

**Anti-patterns (never do these):**

| Anti-pattern | Why it breaks | Do this instead |
|---|---|---|
| Create your own `<ThemeProvider>` | Overrides the host theme cascade | Use `useTheme()` to read the existing theme |
| Hardcode hex colors (`#00A9E0`) | Breaks if theme changes, inconsistent | Use `theme.palette.primary.main` |
| Import from `@mui/material` without bundling it | Not externalized by host — bloats bundle or crashes | Bundle as dependency (opt-in v5 path) or use `@material-ui/core` |
| Use `sx` prop without bundled v5 | MUI v5 only — does nothing in v4 | Bundle `@mui/material` as dependency, or use `makeStyles`/`className` |
| Inline `style={{}}` for theme values | Bypasses JSS, no responsive support | Use `makeStyles` with theme callback |
| Skip `StylesProvider` in multi-plugin | CSS class collisions in production | Always wrap registered components |

**When to use which pattern:**

| Pattern | Best for | Avoid when |
|---|---|---|
| `makeStyles` | Most components — static + dynamic styles with theme | You only need 1-2 simple CSS properties |
| `useTheme()` | Reading theme values in JS logic (e.g., conditional colors) | You need many CSS properties (use makeStyles instead) |
| `withStyles` | Overriding MUI component defaults (Dialog, Button, etc.) | Building new components from scratch |
| `styled()` (MUI v4) | Wrapping MUI components with theme-aware overrides | Complex multi-class styling (use makeStyles) |
| Inline `className` | Simple one-off layout (e.g., `className="flex"`) | Anything that needs theme tokens |

---

## Component SDK Quick Reference

Condensed reference for quick lookup when building or extending SDK components.

### React Hooks Summary

**Reactive hooks** (re-render on change):

| Hook | Import | Returns | Use Case |
|------|--------|---------|----------|
| `useAuth` | `mystique/hooks/useAuth` | `{ user, context, logout, loggedIn }` | User identity, roles, retailer context, role checks |
| `useQuery` | `mystique/hooks/useQuery` | `[{ fetching, stale, data?, error? }, reexecuteFn]` | GraphQL data fetching (last resort — prefer page query data) |
| `useSettings` | `mystique/hooks/useSettings` | `SettingResponse<T>` | Account/retailer/location settings access |
| `useI18n` | `mystique/hooks/useI18n` | `{ translate, translateOr, prefixedTranslate, language }` | Internationalization and string translation |
| `useData` | `mystique/hooks/useData` | `{ context, variables, loadingState }` | Scoped page/provider data access plus page query variable control |
| `useEnv` | `mystique/hooks/useEnv` | `{ account, app, api }` | Environment details (account name, app name, API endpoint) |
| `useRest` | `mystique/hooks/useRest` | `RestResponse<T>` | Authenticated REST API calls (reactive) |
| `useUserActions` | `mystique/hooks/useUserActions` | `QueryResponse<{ entity, userActions[] }>` | Available workflow user actions for current entity |
| `useUserActionForm` | `mystique/hooks/useUserActionForm` | `{ status, result: { label, form, submit, hasVisibleFields } }` | Generate user action form with auto-dispatch |

**Imperative functions** (Promise-based, for `useEffect`/handlers):

| Function | Import | Returns | Use Case |
|----------|--------|---------|----------|
| `getQuery` | `mystique/hooks/getQuery` | `Promise<{ data?, error? }>` | Conditional/sequential GraphQL fetches, pagination loops |
| `getRest` | `mystique/hooks/getRest` | `Promise<T>` | Event dispatch, job/batch API, any REST call |
| `getSettings` | `mystique/hooks/getSettings` | `Promise<SettingResponse<T>>` | Dynamic setting names, explicit retailer/location scope |
| `getApiDownload` | `mystique/hooks/getApiDownload` | `Promise<Blob>` | File downloads (labels, reports) |

### Field Component Pattern

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

const MyField: React.FC<FormFieldProps<string>> = ({ value, onChange, disabled, error }) => {
  // CRITICAL: Wrap onChange in useEffect to prevent infinite loops.
  // Calling onChange directly in the render body triggers a parent
  // re-render which re-renders this component, creating an infinite cycle.
  useEffect(() => {
    if (!value && defaultValue) {
      onChange(defaultValue);
    }
  }, []);

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

FieldRegistry.register(['acme.fields.my-field'], MyField);
```

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

Data fetching is where teams cause the most damage. Follow the tier hierarchy strictly.

**Tier 1 — Free (zero network cost, always prefer):**
- `useData()` — Page query result already in memory. Always try this first.
- `useAuth()` — Session data (user, roles, context). Already loaded.
- `useSettings()` — Cached per session. Settings rarely change mid-session.

**Tier 2 — Cheap (expand the manifest page query):**
- Add fields to the manifest `data.query` instead of calling `useQuery()`. One query, shared across all descendants via `useData()`.

**Tier 3 — Expensive (last resort):**
- `useQuery()` — Reactive GraphQL call. Re-renders on response. Only when data is NOT in page query.
- `getQuery()` — Imperative GraphQL call (Promise). Use in `useEffect`/handlers for conditional or sequential fetches.
- `getRest()` / `useRest()` — REST API calls. For event dispatch (`api/v4.1/event/sync`), job/batch operations, downloads.

**Reactive vs Imperative choice:**
- Use `useQuery` when the component always needs this data on mount
- Use `getQuery` when fetching depends on another async result, user action, or cursor pagination loop
- Use `getRest` for event dispatch and REST-only endpoints (jobs, batches, file downloads)

**Key anti-patterns:** N+1 queries (each list item calls `useQuery()` — fix by expanding page query), unbounded connections (always use `first: N` on edges), over-fetching deep entity graphs (select only fields you display), storing entity data in component state (use `useState` for UI state only — expanded/collapsed, selected tab).

**No shared query cache:** Two components calling `useQuery()` with identical query+variables make two separate API calls. This is the strongest argument for Tier 1/2 over Tier 3.

### Material-UI Constraint

The host Fluent OMX application provides **Material-UI v4.12.4** as a global (`window.MaterialUI`), externalized via webpack. This is the default and lowest-cost option.

**Default path (v4 externalized — recommended):**
- Import from `@material-ui/core` — uses the host-provided global (zero bundle cost)
- Use `makeStyles`, `useTheme`, `withStyles` from `@material-ui/core/styles`
- `useTheme` returns a v4 theme with Fluent extensions (`palette.mystique.colours.*`)
- **Never create your own `ThemeProvider`** — the Mystique shell provides the theme

**Bundled v5 path (opt-in — for newer projects):**
- You CAN use `@mui/material` (v5+) if you add it as a **bundled dependency** (NOT externalized)
- This increases bundle size (~100-200KB) but gives access to `sx` prop, `styled()`, v5 components, `@mui/x-date-pickers`, etc.
- **Do NOT externalize** `@mui/material` — the host does not provide it as a global
- You can mix v4 (externalized) and v5 (bundled) in the same project — many production implementations do this during migration
- When using v5 `sx` prop, theme tokens are still available via `theme.palette.*`

**Anti-patterns (both paths):**
- Creating your own `<ThemeProvider>` — overrides the host theme cascade
- Hardcoded hex colors instead of `theme.palette.*` tokens
- Missing `StylesProvider` + `createGenerateClassName` wrapping when using `makeStyles` in multi-plugin environments

### Naming Convention (MANDATORY)

All registries use `<namespace>.<segment>.<kebab-name>`:

| Registry | Pattern | Example |
|----------|---------|---------|
| `ComponentRegistry` | `<ns>.<category>.<kebab>` | `appeasement.content.create-appeasement-action` |
| `FieldRegistry` | `<ns>.fields.<kebab>` | `appeasement.fields.amount-calculator` |
| `TemplateRegistry` | `<ns>.helpers.<kebab>` | `appeasement.helpers.format-currency` |

**Rules:** Prefer a custom namespace over `fc.` (which is used by OOTB components). `fc.custom.*` is acceptable but may collide with future OOTB additions — a project-specific namespace (e.g., `acme.*`) is safer. Kebab-case only. One canonical alias per component. Category is MANDATORY for ComponentRegistry (`page` | `layout` | `content` | `filter`).

### Component Category Architecture

Choose the category based on rendering role and data behavior, not visual complexity.

#### Category and Data Behavior Quick Matrix

| Component `category` | Use when | `data` scope | `<Children />` impact | Typical API |
|---|---|---|---|---|
| `page` | Full page shell replacement | Unscoped full page query | Usually required | `useData`, `useUserActions`, `<Children />` |
| `layout` | Arranging descendant components | Scoped by parent `dataSource` | Core pattern | `<Children />`, `dataOverride`, `descendantsOverride` |
| `content` | Widget/card/action component (default) | Scoped by parent `dataSource` | Optional | `<DynamicValue />`, `TemplateRegistry.render`, `useData` |
| `filter` | Updating query variables for search/filter/sort | Scoped + mutable variables | Rarely used | `useData().variables.setVariables()` |

Registry choice:
- `ComponentRegistry` for manifest `"component"` renderers (`page`/`layout`/`content`/`filter`).
- `FieldRegistry` for workflow user action form fields (`attributes[].type` mapping).
- `TemplateRegistry` for helper expressions in `{{ }}` templates.

### Component Registration Patterns

```typescript
// Standard display component (StdProps & YourProps)
import { ComponentRegistry } from 'mystique/registry/ComponentRegistry';
ComponentRegistry.register(['acme.content.my-card'], MyCard, { category: 'content' });

// Form field component — matched by workflow attribute type
import { FieldRegistry } from 'mystique/registry/FieldRegistry';
FieldRegistry.register(['acme.fields.stock-reservation'], StockReservation);

// Field with custom props superset — as any cast is safe when your props extend FormFieldProps
FieldRegistry.register(['acme.fields.amount-calculator'], AmountCalculator as any);

// Template helper (available in {{}} expressions)
import { TemplateRegistry } from 'mystique/registry/TemplateRegistry';
TemplateRegistry.register('acme.helpers.format-currency', formatCurrencyFn);
```

### Props Interfaces — StdProps vs FormFieldProps\<T\>

| Interface | Registry | Receives `data`? | Key Fields |
|-----------|----------|:-:|---|
| `StdProps & YourProps` | ComponentRegistry | Yes (page query) | `data`, plus any manifest `props` |
| `FormFieldProps<T>` | FieldRegistry | No | `value`, `onChange`, `error`, `disabled`, `options`, `entityContext` |

**When `as any` is needed:** If your field component uses a custom `FieldProps<T>` that extends `FormFieldProps<T>` with additional props (like `data`), the type assertion `as any` is required in the registration call. This is safe — Mystique passes compatible props at runtime.

**When `as any` is NOT needed:** If your component directly implements `FC<FormFieldProps<T>>`.

### Registration Order

Order does NOT matter. Each `register()` call is independent — adding to a global map. A failed registration does not prevent subsequent ones. Recommended order for readability: ComponentRegistry → FieldRegistry → TemplateRegistry.

### Custom Hooks Pattern

Custom hooks compose built-in Mystique hooks into domain-specific operations:

```typescript
export function useAppeasementReasons(settingName: string) {
  const [result] = useQuery<any>(SETTINGS_QUERY, { name: [settingName] });
  if (result.fetching) return { reasons: [], loading: true };
  const raw = result.data?.settings?.edges?.[0]?.node?.lobValue;
  return { reasons: Array.isArray(raw) ? raw : [], loading: false };
}
```

**Rules:** Return typed objects (not tuples). Handle loading/error states. Use `useQuery` for GraphQL, `useSettings` for key-value. Document with JSDoc.

### Key Best Practices

- **Wrap `onChange` in `useEffect`** — Direct `onChange` calls in render cause infinite re-render loops
- **Use `<DynamicValue>` for user-configurable output** — Keeps components flexible and reusable
- **Use `<Children />` for composable containers** — Lets manifest configurers nest arbitrary components inside yours
- **Type everything with TypeScript** — Avoid `any` types; use `FormFieldProps<T>` for fields (cast only when extending)
- **Handle loading and error states** — Every `useQuery` call needs loading spinner and error display
- **Use local `./` imports** — Imports without `./` prefix fail in the webpack externals setup
- **React 17 is the baseline** — The host provides React 17. Newer SDK versions (v1.2024.9+) support React 18 but avoid concurrent-only APIs (`useId`, `startTransition`) unless you've confirmed your SDK version supports them
- **One canonical alias** — Register each component/field with exactly one alias in the `<ns>.<segment>.<kebab>` format

---

## Knowledge References

- `/fluent-mystique-scaffold` SKILL.md — Project scaffolding, file templates, build lifecycle
- `/fluent-mystique-component` SKILL.md — Adding components to existing projects
- `/fluent-mystique-builder` components/index.md — OOTB component alias catalog
- `/fluent-mystique-builder` SKILL.md — Template helper reference plus manifest patterns
- `knowledge/platform/user-action-contract.md` — User action ↔ field contract details
