---
name: mystique-sdk
scope: frontend
description: "Component SDK: hooks, registration, testing, deployment"
load: on-demand
relevant-skills: [fluent-mystique-scaffold, fluent-mystique-builder]
---

# Mystique Component SDK Development Reference

Comprehensive guide for building custom React components that extend Fluent Commerce web applications using the Component SDK.

---

## 1. Project Setup

### Installation

1. Ask first: do you already have an OMX SDK source repo/folder to extend? If yes, use that source directly (do not re-download).

2. If no source exists, download the latest SDK:
   ```
   https://fluent.apps.fluentcommerce.com/_sdk/latest/omx-component-sdk.zip
   ```

3. Extract the zip, then place/keep the project under:
   - `accounts/<PROFILE>/SOURCE/frontend/<project-name>/` for Mystique SDK projects
   - `accounts/<PROFILE>/SOURCE/backend/<project-name>/` for Java/module/plugin projects

4. Install dependencies:
   ```bash
   yarn install
   ```

5. Start the development server:
   ```bash
   yarn start
   # Runs on http://localhost:3001 by default
   # Use --port xxxx to change port when running multiple plugins
   ```

### Project Structure

```
accounts/<PROFILE>/SOURCE/frontend/<project-name>/
├── src/
│   ├── components/          # Standard display components
│   │   ├── Hello.tsx        # Sample component
│   │   └── MyComponent.tsx
│   ├── fields/              # Form field components
│   │   └── StockReservation.tsx
│   ├── templates/           # Template helpers
│   │   └── customHelper.ts
│   ├── types/               # Type definitions
│   │   └── myTypes.ts
│   └── index.tsx            # Registration entry point
├── @types/
│   └── mystique/            # Platform type declarations
│       ├── hooks.d.ts       # Hook types
│       ├── types.d.ts       # Component types
│       └── registry.d.ts    # Registry types
├── package.json
├── tsconfig.json
└── webpack.config.js
```

### Manifest Plugin Configuration

**Development (localhost):**
```json
{
  "plugins": [
    { "type": "url", "src": "http://localhost:3001/" }
  ]
}
```

**Production (CDN):**
```json
{
  "plugins": [
    { "type": "url", "src": "https://cdn.example.com/my-plugin/v1.2.0/" }
  ]
}
```

### Where Configuration Lives (Source of Truth)

| Concern | Configure In | Notes |
|---|---|---|
| Component/field/helper registration | `accounts/<PROFILE>/SOURCE/frontend/<project>/src/index.tsx` | `ComponentRegistry`, `FieldRegistry`, `TemplateRegistry` |
| Runtime plugin loading | Manifest `plugins[]` in `fc.mystique.manifest.<appname>` / fragment settings | Use plugin folder URL with trailing `/` for `version.json` auto-discovery |
| User action form schema | Workflow `userActions[].attributes[]` | Manifest does not redefine field schema |
| User action module visibility | Workflow `userActions[].context[].modules[]` | OMS App uses legacy alias `"adminconsole"` via `orchestrationAlias`, Store App uses `"servicepoint"` |
| Field type override | Workflow attribute `type` + matching `FieldRegistry.register()` key | Same type string must be used in both places |
| Manifest deployment | GraphQL `createSetting`/`updateSetting` or CLI `fluent module install --include settings` | Use `valueType: "JSON"`, `context: "ACCOUNT"`, `contextId: 0`, body in `lobValue` |

### Approval Gate + End-to-End Loop

1. Plan and approval first (`/fluent-feature-plan` for multi-artifact work, or approved standalone UI plan).
2. Build (`/fluent-mystique-scaffold` or `/fluent-mystique-component` + `/fluent-mystique-builder`).
3. Local verification first (`/fluent-frontend-build dev` + localhost plugin `http://localhost:3001/`).
4. Validate (`/fluent-mystique-validate`) and run release gates (`/fluent-frontend-build full` + `/fluent-frontend-review`).
5. Deploy release artifacts (settings + CDN plugin bundle).
6. Verify in browser (`/fluent-ui-test`; optionally `/fluent-ui-record` for evidence).
7. If failure: triage root cause, fix, and re-run the same test case until PASS.
   - Code-only fix with localhost plugin active: no manifest redeploy needed
   - Manifest/workflow/plugin URL change: redeploy changed artifact(s) before re-test

---

## 2. Component Registration

### ComponentRegistry (Standard Components)

Standard components are display components registered for use in manifests.

```typescript
// src/components/MyComponent.tsx
import { FC } from 'react';
import { TemplateRegistry } from 'mystique/registry/TemplateRegistry';
import { Card } from 'mystique/components/Card';
import { CardContent } from 'mystique/components/CardContent';

interface MyComponentProps {
  title?: string;
  content?: string;
}

export const MyComponent: FC<MyComponentProps> = ({ data, title, content }) => {
  return (
    <Card title={title}>
      <CardContent>
        <p>{TemplateRegistry.render(content, data)}</p>
      </CardContent>
    </Card>
  );
};

// src/index.tsx
import { ComponentRegistry } from 'mystique/registry';
import { MyComponent } from './components/MyComponent';

ComponentRegistry.register(
  ['acme.content.my-component'],  // Alias: <namespace>.<category>.<kebab-name>
  MyComponent,                     // Component implementation
  { category: 'content' }         // Category: page | layout | content | filter
);
```

**Category determines how the host treats your component** — data injection, rendering context, Children support, and query variable access all depend on it:

| Category | When to Use | Data Access | `<Children />` |
|----------|------------|------------|----------------|
| `page` | Full page replacement (route-level) | Route query, URL params, `useData().variables` | Yes — manages page layout |
| `layout` | Arranges/composes other components (grids, tabs) | Inherited from parent via `StdProps.data`, `useData()` | Yes — renders descendants |
| `content` | Displays information (cards, tables, charts) — **80%+ of custom components** | `StdProps.data` auto-scoped by `dataSource` chain | Rarely — leaf node |
| `filter` | Modifies query variables (search, date range, status) | `useData().variables.setVariables()` to update page query | No — standalone input |

> **Full decision tree, per-category props interfaces, data flow diagrams, and common mistakes:** See `knowledge/frontend/mystique-advanced-sdk.md` §1-2.

**Manifest usage:**
```json
{
  "component": "acme.content.my-component",
  "dataSource": "order",
  "props": {
    "title": "Customer Details",
    "content": "{{customer.firstname}} {{customer.lastname}}"
  }
}
```

### FieldRegistry (Form Field Components)

Field components produce `onChange` events for user action forms. They are automatically selected when the workflow attribute type matches the registered field name.

```typescript
// src/fields/StockReservation.tsx
import { FC, useState, useEffect } from 'react';
import { FormFieldProps } from 'mystique/registry/FieldRegistry';

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

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

  // CRITICAL: Wrap onChange in useEffect to avoid infinite loops
  useEffect(() => {
    onChange({ locationRef, quantity });
  }, [locationRef, quantity]);

  return (
    <div>
      <TextField
        label="Location"
        value={locationRef}
        onChange={(e) => setLocationRef(e.target.value)}
        disabled={disabled}
      />
      <TextField
        label="Quantity"
        type="number"
        value={quantity}
        onChange={(e) => setQuantity(Number(e.target.value))}
        disabled={disabled}
      />
    </div>
  );
};

// src/index.tsx
import { FieldRegistry } from 'mystique/registry/FieldRegistry';
import { StockReservation } from './fields/StockReservation';

FieldRegistry.register(
  ['acme.fields.stock-reservation'],
  StockReservation
);
```

**Workflow user action configuration that triggers this field:**
```json
{
  "userActions": [
    {
      "context": [
        {
          "label": "Reserve Stock",
          "type": "PRIMARY",
          "modules": ["adminconsole", "store"],
          "confirm": true
        }
      ],
      "attributes": [
        {
          "name": "stockReservation",
          "label": "Stock Reservation",
          "type": "acme.fields.stock-reservation",
          "source": "",
          "defaultValue": "",
          "mandatory": false
        }
      ]
    }
  ]
}
```

### FieldRegistry Data Access in User Action Dialogs

**CRITICAL LIMITATION:** FieldRegistry fields in user action dialogs receive NO page data:

| Data source | In dialog | Why |
|---|---|---|
| `data` prop | `undefined` | FieldRegistry fields don't receive this (unlike ComponentRegistry) |
| `useData()` | `{ context: { data: {} } }` | Dialog renders outside page's React data context provider |
| `useQuery()` | Works | Self-contained query, works in any React context |

**If a custom field needs entity data** (e.g., order line items for an amount calculator), it MUST use `useQuery()`. Prefer `entityContext?.[0]?.entity.id` for the entity ID because it is framework-provided and survives route changes better than manual hash parsing. Fall back to `window.location.hash` only when `entityContext` is unavailable or you specifically need values not carried in `entityContext` (for example `retailerId`). The query must be validated via MCP `graphql.validate` before embedding in source.

```typescript
const MyField: FC<FormFieldProps<string>> = ({ entityContext, onChange }) => {
  const entityId = entityContext?.[0]?.entity?.id ?? null;
  const [result] = useQuery(RELATED_QUERY, entityId ? { id: entityId } : undefined);
  // ... render using result.data
};
```

**Schema gotchas for Order queries:**
- `Order.totalPrice` is scalar `Float` — no sub-selections allowed
- `Order` has NO `currency` field — check `OrderItem.currency` instead
- `OrderItem.product` is NON_NULL in schema but can be null at runtime (broken refs)

### User Action Field Resolution (Contract)

Use this data flow when debugging what renders in Mystique:

1. Workflow `userActions[].attributes[]` defines field schema (`name`, `label`, `type`, `source`, `defaultValue`, `mandatory`).
2. Transition API returns those attributes for the current entity status.
3. Mystique maps `attributes[].type` to a registered `FieldRegistry` key.
4. The selected field receives values via `FormFieldProps` (`value`, `defaultValue`, `source`, `error`, `disabled`).

Manifest configuration does not redefine user action form fields. Manifest overrides only affect button-level behavior (for example label, condition, order), not field contract details.

### TemplateRegistry (Template Helpers)

Template helpers follow the Handlebars framework conventions.

```typescript
// src/templates/formatCurrency.ts
export const formatCurrency = (amount: number, currency: string): string => {
  return `${currency} ${Number(amount).toFixed(2)}`;
};

// src/index.tsx
import { TemplateRegistry } from 'mystique/registry/TemplateRegistry';
import { formatCurrency } from './templates/formatCurrency';

TemplateRegistry.register('appeasement.helpers.format-currency', formatCurrency);
```

**Manifest usage:**
```json
{ "value": "{{appeasement.helpers.format-currency node.totalPrice 'AUD'}}" }
```

> **Note:** `TemplateRegistry.register()` accepts `string | string[]` as the first parameter (unlike `ComponentRegistry`/`FieldRegistry` which require `string[]`). Use the `<namespace>.helpers.<kebab-name>` convention for consistency with the other registries.

### Three-Registry Comparison

Use this table to decide which registry to use for a given requirement:

| Registry | Purpose | Use When | Register With | Manifest Usage |
|----------|---------|----------|---------------|----------------|
| ComponentRegistry | UI display components | Need custom visual layout, data display, or interaction | `ComponentRegistry.register(['alias'], Component, {category})` | `"component": "alias"` in descendants |
| FieldRegistry | Form input fields | Need custom form field for user action attributes | `FieldRegistry.register(['type.name'], FieldComponent)` | Auto-selected by `eventAttributes[].type` match |
| TemplateRegistry | Data transformation helpers | Need to format/transform/aggregate data in `{{}}` template expressions | `TemplateRegistry.register('ns.helpers.name', fn)` | `"{{ns.helpers.name arg1 arg2}}"` in template strings |

**Decision flowchart:**

```
Need custom UI rendering?                              → ComponentRegistry
Need custom form input for user actions?                → FieldRegistry
Need data formatting/transformation in {{}} expressions? → TemplateRegistry
```

**Helper signature spec:** Helpers receive arguments positionally from the template expression (e.g., `{{helperName arg1 arg2}}` passes `arg1` and `arg2` as the first two parameters). Helpers return `string`, `number`, or `boolean`. If a helper throws an error or returns `undefined`, the template renders the raw `{{expression}}` string unchanged. Block helpers (e.g., `{{#ifRole}}...{{/ifRole}}`) additionally receive a Handlebars `options` object as the last parameter and must use `function` (not arrow) syntax to access `this` context.

**Registration parameter difference:** `TemplateRegistry.register()` accepts `string | string[]` as the first argument. `ComponentRegistry.register()` and `FieldRegistry.register()` both require `string[]` (array only). This means single-alias template helpers can skip the array wrapper: `TemplateRegistry.register('ns.helpers.name', fn)`.

### Component Category Decision Tree

The `category` parameter in `ComponentRegistry.register()` tells the host what role the component plays. Source: `@types/mystique/registry.d.ts` lines 21-28. For the full decision tree with data flow diagrams, common mistakes, and architecture deep-dive, see `knowledge/frontend/mystique-advanced-sdk.md` §1-2. For registration code examples per category, see `/fluent-mystique-sdk-reference`.

| Category | Purpose | `data` contains | Key patterns | Use `<Children />`? |
|----------|---------|----------------|--------------|-------------------|
| `page` | Replaces entire page layout (route-level) | Full page query result (unscoped) | useData, useUserActions, `<Children />` | Yes (mandatory) |
| `layout` | Structures/arranges other components | Scoped by parent's `dataSource` | `<Children />` with dataOverride/descendantsOverride | Yes (core purpose) |
| `content` | Displays information or provides interactive widget | Scoped by parent's `dataSource` + custom props | `<DynamicValue />`, TemplateRegistry.render() | Optional |
| `filter` | Modifies query variables to filter/sort data | Scoped by parent's `dataSource` | useData().variables.setVariables() | No |

**When in doubt, use `content`.** It's the correct choice for 80%+ of custom components. Use `page` only when replacing the entire page rendering. Use `layout` only when the component's primary job is composing children. Use `filter` only when the component modifies query variables.

**Category applies to ComponentRegistry only.** FieldRegistry and TemplateRegistry do not use categories. Form fields are always user-action-scoped; template helpers are always expression-scoped. Only display components need the host to know their structural role.

**`dataSource` scoping**: When manifest has `"dataSource": "order"`, Mystique extracts `data.order` and passes it as `data` to that component and its descendants. Nested scoping chains: parent `"order"` + child `"items"` → child receives `data.order.items`.

**`<Children />` indexed slots**: Layout components can render 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 all children: `<Children dataOverride={filteredData} />`.

**`filter` re-fetch behavior**: When a filter component 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. The second argument (`true`) merges with existing variables; `false` replaces them entirely.

**`page` owns user actions**: Page-level components are the only category that should call `useUserActions()` — they own the entity detail rendering where workflow action buttons appear. Content and layout components receive data but don't manage the action bar.

**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 |

### Alias Naming Rules (from registry.d.ts)

| Rule | Detail |
|------|--------|
| **Reserved `fc.`** | NEVER use `fc.*` — reserved for Fluent OOTB. Your component silently overrides the built-in. |
| **ComponentRegistry** | `<namespace>.<category>.<kebab-name>` — e.g., `acme.content.order-card` |
| **FieldRegistry** | `<namespace>.fields.<kebab-name>` — same pattern as ComponentRegistry. Workflow attribute `type` must match. |
| **TemplateRegistry** | `<namespace>.helpers.<kebab-name>` — accepts `string \| string[]` (unlike the others which require `string[]`) |
| **kebab-case** | Use `order-card` not `OrderCard` in aliases |
| **Single canonical alias** | One component = one primary alias. Extra aliases only for backward compat. |

### FieldRegistry Naming (Single Canonical Alias)

```typescript
// CORRECT — one canonical alias, kebab-case
FieldRegistry.register(['acme.fields.amount-calculator'], MyField as any);
// Workflow attribute: "type": "acme.fields.amount-calculator"

// MIGRATION ONLY — dual alias for backward compatibility with existing workflows
FieldRegistry.register(['legacyName', 'acme.fields.new-name'], MyField as any);
// Remove the legacy alias once all workflows are updated
```

The workflow attribute `type` must match the registered alias exactly. When renaming, temporarily register both old and new aliases, then remove the old one after all workflows are updated.

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

The SDK defines two distinct prop contracts — one for display components and one for form fields. Understanding the difference is critical.

**StdProps (ComponentRegistry components):**

```typescript
// From @types/mystique/registry.d.ts
export interface StdProps {
  data?: any;  // Page-query data, scoped by dataSource in manifest
}
```

Display components receive `StdProps` plus any custom props defined in the manifest `"props": {}` block. The `data` field is injected by the Mystique host — it contains the page query result, scoped by `dataSource` from the manifest.

```typescript
// Your component props = StdProps & YourProps
interface MyComponentProps {
  title?: string;    // Comes from manifest props
  maxItems?: number; // Comes from manifest props
}

// Registration signature: FC<StdProps & T>
ComponentRegistry.register(['ns.content.my-component'], MyComponent, { category: 'content' });
```

**FormFieldProps\<T\> (FieldRegistry fields):**

```typescript
// From @types/mystique/registry.d.ts — key fields:
export interface FormFieldProps<T> extends FormField<T> {
  onChange: (value: T, options?: OnChangeOptions) => void;  // Push value to parent form
  onBlur?: () => void;                                       // Trigger validation
  error?: string;                                            // Validation error
  entityContext?: FormEntityContext[];                        // Target entity info
  // Inherited from FormField<T>:
  name: string;           // Field name (from workflow attribute)
  label: string;          // Display label
  value?: T;              // Current value
  defaultValue?: T;       // Initial value
  options?: FormFieldOption<T>[];  // Dropdown options (from source: "settings.*")
  helperText?: string;    // Help text
  validation?: Record<string, any>;  // Validation config
}
```

Form fields receive NO `data` prop from the host — they operate within the user action dialog context, not the page query context. To access page data inside a field, define a standalone props interface:

```typescript
// Actual pattern from real projects (e.g., appeasement amount calculator):
export interface FieldProps<T = string> {
  value: T;
  onChange: (value: T) => void;
  disabled?: boolean;
  error?: string;
  label?: string;
  required?: boolean;
  data?: any;   // Page data — NOT in FormFieldProps<T>
}
```

This standalone `FieldProps<T>` is preferred over `extends FormFieldProps<T>` in practice because:
- `FormFieldProps<T>` requires `name` and `label` as mandatory fields, adding noise to component signatures
- Custom fields often need `data` (page query) which `FormFieldProps<T>` doesn't include
- The `onChange` signature differs: `FormFieldProps<T>` passes `OnChangeOptions` as a second parameter; custom fields typically use a simpler `(value: T) => void`

**Why `as any` is needed in FieldRegistry registration:**

```typescript
// The SDK signature expects FC<FormFieldProps<T>>:
FieldRegistry.register<T>(aliases: string[], field: FC<FormFieldProps<T>>): void;

// But your component uses a standalone FieldProps<T>:
export const MyField: React.FC<FieldProps<string>> = ({ value, onChange, data }) => { ... };

// Type assertion needed: FieldProps is structurally compatible but not a TS subtype
FieldRegistry.register(['ns.fields.my-field'], MyField as any);
```

This `as any` is safe because at runtime Mystique passes compatible props to the component. The standalone pattern is what real SDK projects use (see appeasement `src/types.ts` for the canonical example).

**When NOT to use `as any`:** If your component directly implements `FC<FormFieldProps<T>>` with no extensions, no cast is needed.

### OnChangeOptions — Confirm, Summary, and isInvalid

FieldRegistry fields can interact with the parent form beyond just providing values:

```typescript
onChange(value, {
  confirm: { title: 'Confirm', content: <p>Are you sure?</p> },  // Shows modal on submit
  summary: { content: <p>Total: $35.00</p> },                     // Floating summary in form
  isInvalid: true,  // Prevent form submission even if field is optional
});
```

### Registration Order in src/index.tsx

**Order does NOT matter for correctness.** Each `register()` call is independent — it adds the component/field/helper to a global map keyed by alias. There are no dependencies between registrations. A failed registration (e.g., import error) does NOT prevent subsequent registrations from executing, because the host catches and logs errors per-registration.

**Recommended order** (for readability only):
1. ComponentRegistry registrations (grouped by category)
2. FieldRegistry registrations
3. TemplateRegistry registrations

```typescript
// src/index.tsx — recommended structure
import { ComponentRegistry } from 'mystique/registry/ComponentRegistry';
import { FieldRegistry } from 'mystique/registry/FieldRegistry';
import { TemplateRegistry } from 'mystique/registry/TemplateRegistry';

// Components
import { MyCard } from './components/MyCard/MyCard';
import { MyPage } from './components/MyPage/MyPage';

// Fields
import { MyCalculator } from './fields/MyCalculator/MyCalculator';

// Helpers
import { formatCurrency } from './templates/formatCurrency';

// --- ComponentRegistry ---
ComponentRegistry.register(['ns.content.my-card'], MyCard, { category: 'content' });
ComponentRegistry.register(['ns.page.my-page'], MyPage, { category: 'page' });

// --- FieldRegistry ---
FieldRegistry.register(['ns.fields.my-calculator'], MyCalculator as any);

// --- TemplateRegistry ---
TemplateRegistry.register('ns.helpers.format-currency', formatCurrency);
```

### Building Custom Hooks

Custom hooks encapsulate reusable data-fetching or business logic. They compose Mystique's built-in hooks (`useQuery`, `useSettings`, `useAuth`) into domain-specific operations.

**Pattern 1: Settings-driven data (useQuery + useState/useEffect)**

```typescript
// src/hooks/useAppeasementReasons.ts
import { useState, useEffect } from 'react';
import { useQuery } from 'mystique/hooks';

const REASONS_QUERY = `
  query AppeasementReasons($name: [String]) {
    settings(name: $name, first: 1) {
      edges { node { name lobValue } }
    }
  }
`;

interface UseAppeasementReasonsResult {
  reasons: string[];
  loading: boolean;
  error: string | null;
}

/**
 * Fetches appeasement reason options from a Fluent setting.
 * Uses useState/useEffect to parse the query result into a typed array.
 * @param settingName - The setting key (e.g., "APPEASEMENT_REASONS")
 * @returns { reasons, loading, error }
 */
export function useAppeasementReasons(
  settingName = 'APPEASEMENT_REASONS'
): UseAppeasementReasonsResult {
  const [reasons, setReasons] = useState<string[]>([]);
  const [error, setError] = useState<string | null>(null);

  const [result, { loading }] = useQuery(REASONS_QUERY, { name: [settingName] });

  useEffect(() => {
    if (!result) return;
    try {
      const edges = result?.settings?.edges;
      if (!edges || edges.length === 0) {
        setError(`Setting "${settingName}" not found.`);
        return;
      }
      const lobValue = edges[0]?.node?.lobValue;
      // lobValue may be a string (needs parsing) or already an object
      const parsed = typeof lobValue === 'string' ? JSON.parse(lobValue) : lobValue;
      if (!Array.isArray(parsed)) {
        setError(`Setting "${settingName}" is not a valid JSON array.`);
        return;
      }
      setReasons(parsed);
      setError(null);
    } catch (e) {
      setError(`Failed to parse "${settingName}": ${(e as Error).message}`);
    }
  }, [result, settingName]);

  return { reasons, loading, error };
}
```

**Pattern 2: Role-based logic (useQuery + user data cross-reference)**

```typescript
// src/hooks/useUserLimit.ts
import { useState, useEffect } from 'react';
import { useQuery } from 'mystique/hooks';

interface UserAppeasementInfo {
  role: string | null;
  limit: number | null;  // null = unlimited
  allRoles: string[];
}

interface UseUserLimitResult {
  userInfo: UserAppeasementInfo;
  loading: boolean;
  error: string | null;
}

const LIMITS_QUERY = `
  query AppeasementLimits($name: [String]) {
    settings(name: $name, first: 1) {
      edges { node { name lobValue } }
    }
  }
`;

/**
 * Resolves the current user's appeasement role and dollar limit.
 * Cross-references `me` query roles with a settings-based role-to-limit map.
 * @param meData - Page query `me` data (username, roles)
 * @param limitsSettingName - Setting key with role-to-limit JSON map
 * @param rolePrefix - Prefix to match (e.g., "APPEASEMENT_")
 */
export function useUserLimit(
  meData: { roles?: { edges: Array<{ node: { role: { name: string } } }> } } | undefined | null,
  limitsSettingName = 'appeasement.limits',
  rolePrefix = 'APPEASEMENT_'
): UseUserLimitResult {
  const [userInfo, setUserInfo] = useState<UserAppeasementInfo>({
    role: null, limit: null, allRoles: [],
  });
  const [error, setError] = useState<string | null>(null);

  const [limitsResult, { loading: limitsLoading }] = useQuery(LIMITS_QUERY, {
    name: [limitsSettingName],
  });
  const loading = limitsLoading || !meData;

  useEffect(() => {
    if (!meData || !limitsResult) return;
    try {
      const allRoles = meData.roles?.edges?.map((e) => e.node.role.name) ?? [];
      const appeasementRole = allRoles.find((r) => r.startsWith(rolePrefix)) ?? null;

      const edges = limitsResult?.settings?.edges;
      if (!edges || edges.length === 0) {
        setUserInfo({ role: appeasementRole, limit: null, allRoles });
        setError(`Setting "${limitsSettingName}" not found.`);
        return;
      }
      const lobValue = edges[0]?.node?.lobValue;
      const limitsMap = typeof lobValue === 'string' ? JSON.parse(lobValue) : lobValue;
      const roleLimit = appeasementRole && appeasementRole in limitsMap
        ? limitsMap[appeasementRole] : null;

      setUserInfo({ role: appeasementRole, limit: roleLimit, allRoles });
      setError(null);
    } catch (e) {
      setError(`Failed to resolve user limit: ${(e as Error).message}`);
    }
  }, [meData, limitsResult, limitsSettingName, rolePrefix]);

  return { userInfo, loading, error };
}
```

**Custom hook rules:**
1. Always return a typed result object (not a tuple) for clarity
2. Handle loading and error states — never return undefined
3. Use `useQuery` for GraphQL, `useSettings` for simple key-value settings
4. Wrap side effects in `useEffect` — hooks must be pure during render
5. Document with JSDoc: `@param`, `@returns`, and a one-line description

---

## 3. React Hooks Reference

### useAuth

Access user session, roles, permissions, and context.

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

const MyComponent: FC = () => {
  const auth = useAuth();

  // User information
  auth.user.username;
  auth.user.firstName;
  auth.user.timezone;
  auth.user.language;

  // Current context
  auth.context.current.contextId;     // Retailer or Location ID
  auth.context.current.contextType;   // "RETAILER" or "LOCATION"
  auth.context.current.details?.ref;

  // Roles and permissions
  const hasAdminRole = auth.user.roles.some(
    r => r.role.name === 'ADMIN'
  );

  // Context switching
  auth.switchContext({
    contextType: 'RETAILER',
    contextId: newContextId
  });

  // Logout
  auth.logout();

  return <div>Welcome, {auth.user.firstName}</div>;
};
```

### useQuery

Fetch data from Fluent GraphQL APIs.

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

interface OrderNode {
  id: string;
  ref: string;
  status: string;
  customer: { firstName: string; lastName: string };
}

interface OrderResult {
  orders: Connection<OrderNode>;
}

const orderQuery = `
  query GetOrders($retailerId: [Int!]) {
    orders(retailerId: $retailerId) {
      edges {
        node {
          id
          ref
          status
          customer { firstName lastName }
        }
      }
    }
  }
`;

const MyComponent: FC = () => {
  const auth = useAuth();
  const [result, { loading, error, refetch }] = useQuery<OrderResult>(
    orderQuery,
    { retailerId: auth.context.current.contextId }
  );

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;

  return (
    <div>
      {result.data?.orders.edges.map(({ node }) => (
        <div key={node.id}>
          {node.ref} - {node.customer.firstName}
        </div>
      ))}
      <button onClick={refetch}>Refresh</button>
    </div>
  );
};
```

**Important:** For ComponentRegistry components, prefer page query data over direct `useQuery` when possible. Page query data is already passed into each component via the `data` property, and scoping can be controlled via the `dataSource` field in the manifest. **Exception:** FieldRegistry fields in user action dialogs MUST use `useQuery()` — both `data` prop and `useData()` return empty in dialog context.

### useSettings

Access account/retailer/location settings. Settings cascade: Location -> Retailer -> Account -> Global.

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

const MyComponent: FC = () => {
  const settings = useSettings({ key: 'i18n.languages' });

  if (settings.key.status === 'loading') {
    return <div>Loading settings...</div>;
  }

  if (settings.key.status === 'error') {
    return <div>Error loading settings</div>;
  }

  const languages = settings.key.result?.value;

  return (
    <ul>
      {languages?.map((lang: any) => (
        <li key={lang.value}>{lang.label}</li>
      ))}
    </ul>
  );
};
```

### useI18n

Translate strings based on user language. Language dictionaries are loaded from a setting based on user preference (e.g., `LANGUAGE_JA-JP` for Japanese).

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

const MyComponent: FC = () => {
  const { translate, translateOr, languages, currentLanguage } = useI18n();

  return (
    <div>
      <h2>{translate('fc.om.orders.title')}</h2>

      {/* Fallback chain if key not found */}
      <p>{translateOr([
        'acme.errors.specificError',
        'acme.errors.genericError',
        'An error occurred'  // Default
      ])}</p>

      {/* Current language */}
      <div>Language: {currentLanguage.label}</div>

      {/* Available languages */}
      <select>
        {languages.map(lang => (
          <option key={lang.value} value={lang.value}>
            {lang.label}
          </option>
        ))}
      </select>
    </div>
  );
};
```

### useData

Access the page query response and variables. Use when building components that interact with the page-level query (filters, list controls).

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

const MyComponent: FC = () => {
  const { data, variables, loading, error } = useData();

  // Access page query data
  console.log(data.order);
  console.log(data.order.items);

  // Access query variables
  console.log(variables.orderId);

  return (
    <div>
      {data.order && <div>Order: {data.order.ref}</div>}
    </div>
  );
};
```

### useEnv

Access environment details.

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

const MyComponent: FC = () => {
  const env = useEnv();

  console.log(env.account);  // Account name
  console.log(env.app);      // App name ("oms", "store", etc.)
  console.log(env.api);      // API endpoint

  return <div>Account: {env.account}</div>;
};
```

### useRest / getRest — REST API Access

Make authenticated REST calls against the Fluent API. Domain is assumed — endpoint starts after the domain.

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

// Hook-based (reactive — re-renders on response)
const response = useRest<EventResponse>('/api/v4.1/event/sync', 'POST', eventBody);
// response.status: 'loading' | 'ok' | 'error'

// Promise-based (imperative — for dynamic endpoints in useEffect)
useEffect(() => {
  getRest<LabelData>(`/api/v4/consignment/${id}/label`).then(setLabel);
}, [id]);
```

Only `GET` (default) and `POST` supported. Auth headers added automatically.

### getQuery — Imperative GraphQL Fetching

Promise-based counterpart to `useQuery`. Use when fetching conditionally (on click, after another async call, or inside loops).

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

// Inside useEffect or event handler — NOT during render
const handleClick = async () => {
  const result = await getQuery<{ orderById: Order }>(orderQuery, { id: orderId });
  setOrder(result.data?.orderById);
};
```

**When to use:** conditional fetches, chained queries, pagination loops, event handlers. See `knowledge/frontend/mystique-advanced-sdk.md` §14 for full patterns.

### getApiDownload — File Downloads

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

const blob = await getApiDownload(`api/v4/consignment/${id}/labelStream`);
window.open(URL.createObjectURL(blob));
```

### Frame Utilities — Modals, Drawers, Toasts

Imperative functions to show UI overlays. See `knowledge/frontend/mystique-advanced-sdk.md` §4 for full reference.

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

pushModal({ title: 'Confirm', body: <p>Sure?</p>, actions: [{ label: 'Yes', action: handleYes }] });
pushDrawer({ title: 'Details', body: <DetailView />, actions: [{ label: 'Close', action: clearDrawers }] });
pushToast('success', 'Done', 5000);  // 'success'|'info'|'warning'|'error'
```

---

## 4. Field Component Patterns

### FormFieldProps Interface

All field components receive `FormFieldProps<T>` where `T` is the payload type.

```typescript
interface FormFieldProps<T> {
  onChange: (value: T) => void;   // Emit value changes
  value?: T;                      // Current value
  defaultValue?: T;               // Initial value
  disabled?: boolean;             // Read-only mode
  error?: { message: string };    // Validation error
  source?: string;                // For loading options
}
```

### Critical: Wrap onChange in useEffect

Calling `onChange` directly in a handler causes infinite re-renders because the parent re-renders and passes new props.

```typescript
// GOOD - Prevents infinite loops
const [value, setValue] = useState('');
useEffect(() => {
  onChange({ value });
}, [value]);

// BAD - Causes infinite re-renders
const handleChange = (newValue: string) => {
  setValue(newValue);
  onChange({ value: newValue });  // Triggers parent re-render -> child re-render -> loop
};
```

### Reusing existing field components

Use `FieldRegistry.get()` to compose fields from standard primitives.

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

export const StockReservation: FC<FormFieldProps<Payload>> = ({ onChange }) => {
  const [quantity, setQuantity] = useState(0);
  const QuantitySelector = FieldRegistry.get('number');

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

  return (
    <QuantitySelector
      name="quantity"
      label="Quantity"
      value={quantity}
      onChange={(val) => setQuantity(val)}
    />
  );
};
```

### LocationSelector example (field with data fetching)

Field components appear in many locations and cannot rely on page query data. They must fetch their own data when needed.

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

const locationsQuery = `
  query GetLocations($retailerId: [Int!]) {
    locations(retailerId: $retailerId) {
      edges { node { id ref name } }
    }
  }
`;

export const LocationSelector: FC<FormFieldProps<{locationRef: string}>> = ({
  onChange,
  value
}) => {
  const auth = useAuth();
  const [locationRef, setLocationRef] = useState(value?.locationRef || '');
  const [result] = useQuery(locationsQuery, {
    retailerId: auth.context.current.contextId
  });

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

  if (result.loading) return <Spinner />;

  return (
    <Select value={locationRef} onChange={(e) => setLocationRef(e.target.value)}>
      {result.data?.locations.edges.map(({ node }) => (
        <option key={node.id} value={node.ref}>{node.name}</option>
      ))}
    </Select>
  );
};
```

---

## 5. Data Fetching Patterns

### Pattern 1: Page query data (PREFERRED)

Component receives data from the page query defined in the manifest. No fetch needed.

```typescript
export const MyComponent: FC<ComponentProps> = ({ value, data }) => {
  return <div>{TemplateRegistry.render(value, data)}</div>;
};
```

**When to use:** Always prefer this. Minimises queries per page.

### Pattern 2: useQuery for follow-up queries

Use only when page query data is insufficient.

```typescript
export const MyComponent: FC<ComponentProps> = ({ data }) => {
  const [inventoryResult] = useQuery(inventoryQuery, {
    productRef: data.ref
  });

  return (
    <div>
      {inventoryResult.data && (
        <div>{inventoryResult.data.inventory.quantity}</div>
      )}
    </div>
  );
};
```

**When to use:** The component needs data from a different entity not in the page query.

### Pattern 3: Query with authentication context

Use `useAuth` to get the current context for scoped queries.

```typescript
const MyComponent: FC = () => {
  const auth = useAuth();
  const [result] = useQuery(ordersByLocationQuery, {
    locationId: auth.context.current.contextId
  });
  return <div>{/* ... */}</div>;
};
```

**When to use:** Field components or standalone components that need retailer/location context.

### Pattern 4: Conditional data fetching

Fetch additional data only when the user requests it.

```typescript
const MyComponent: FC<ComponentProps> = ({ data }) => {
  const [showDetails, setShowDetails] = useState(false);
  const [details, setDetails] = useState({});

  useEffect(() => {
    if (showDetails) {
      getQuery(detailsQuery, { id: data.id })
        .then((result) => setDetails(result.data));
    }
  }, [showDetails]);

  return (
    <div>
      <button onClick={() => setShowDetails(true)}>Show Details</button>
      {showDetails && details && (
        <div>{details.order.attributes.byName.additionalDetails}</div>
      )}
    </div>
  );
};
```

**When to use:** Expensive queries that should only fire on user interaction.

---

## 6. Styling

### Material-UI v4.12.4 (PINNED)

Material-UI is provided by the framework and locked to v4.12.4. Anything requiring a later version will break the component (e.g., certain keyframe syntaxes).

### makeStyles

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

const useStyles = makeStyles((theme) => ({
  root: {
    padding: theme.spacing(2),
    backgroundColor: theme.palette.background.paper,
  },
  header: {
    marginBottom: theme.spacing(3),
    color: theme.palette.primary.main,
  },
  button: {
    marginRight: theme.spacing(1),
  },
}));

export const MyComponent: FC = () => {
  const classes = useStyles();
  return (
    <div className={classes.root}>
      <h2 className={classes.header}>Title</h2>
      <button className={classes.button}>Action</button>
    </div>
  );
};
```

### Theme colours

```typescript
const useStyles = makeStyles((theme) => ({
  primary:   { color: theme.palette.primary.main },      // #00A9E0 Fluent blue
  secondary: { color: theme.palette.secondary.main },    // Fluent Brand Purple
  success:   { color: theme.palette.success.main },      // Green
  error:     { color: theme.palette.error.main },        // Red
  warning:   { color: theme.palette.warning.main },      // Yellow
}));
```

### Responsive styles

```typescript
const useStyles = makeStyles((theme) => ({
  container: {
    display: 'flex',
    [theme.breakpoints.down('sm')]: {
      flexDirection: 'column',
    },
    [theme.breakpoints.up('md')]: {
      flexDirection: 'row',
    },
  },
}));
```

### Theme access via hook

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

const MyComponent: FC = () => {
  const theme = useTheme();
  return (
    <div style={{
      padding: theme.spacing(2),
      color: theme.palette.text.primary,
      backgroundColor: theme.palette.background.default,
    }}>
      Content
    </div>
  );
};
```

### Component hierarchy for styling

1. **BEST** -- Fluent components with pre-customised styles (see `@types/mystique/components.d.ts`):
   ```typescript
   import { Card, CardContent } from 'mystique/components/Card';
   import { Loading } from 'mystique/components/Loading';
   ```

2. **GOOD** -- Material-UI components that match the platform:
   ```typescript
   import { Button, Card, TextField } from '@material-ui/core';
   ```

3. **AVOID** -- Custom HTML with inline styles that do not match platform styling.

---

## 7. Third-Party Integration

Prefer React-based, Material-UI compatible, TypeScript-supported libraries.

### Google Maps

```bash
yarn add @react-google-maps/api
```

```typescript
import { GoogleMap, LoadScript, Marker } from '@react-google-maps/api';
import { useSettings } from 'mystique/hooks/useSettings';

export const MapComponent: FC<MapComponentProps> = ({ data, center, zoom }) => {
  const settings = useSettings({ key: 'googleMapsApiKey' });

  if (settings.key.status === 'loading') return <Spinner />;

  const apiKey = settings.key.result?.value;

  return (
    <LoadScript googleMapsApiKey={apiKey}>
      <GoogleMap
        mapContainerStyle={{ width: '100%', height: '400px' }}
        center={center || { lat: -33.8688, lng: 151.2093 }}
        zoom={zoom || 10}
      >
        {data.locations.edges.map(({ node }) => (
          <Marker
            key={node.id}
            position={{ lat: node.coordinates.lat, lng: node.coordinates.lng }}
          />
        ))}
      </GoogleMap>
    </LoadScript>
  );
};
```

**Key pattern:** Use `useSettings` to load the API key -- never hardcode credentials.

### Recharts

```bash
yarn add recharts
```

```typescript
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';

export const ChartComponent: FC<ComponentProps> = ({ data }) => {
  const chartData = data.metrics.edges.map(({ node }) => ({
    date: node.date,
    value: node.value,
  }));

  return (
    <LineChart width={600} height={300} data={chartData}>
      <CartesianGrid strokeDasharray="3 3" />
      <XAxis dataKey="date" />
      <YAxis />
      <Tooltip />
      <Line type="monotone" dataKey="value" stroke="#00A9E0" />
    </LineChart>
  );
};
```

---

## 8. Testing

### Jest configuration

Tests are co-located with components:

```
src/
├── components/
│   ├── MyComponent.tsx
│   └── MyComponent.test.tsx
```

### Basic component test

```typescript
import { render, screen } from '@testing-library/react';
import { MyComponent } from './MyComponent';

describe('MyComponent', () => {
  it('renders component with title', () => {
    const props = {
      data: { order: { ref: 'ORD-123' } },
      config: { title: 'Test Title' }
    };

    render(<MyComponent {...props} />);

    expect(screen.getByText('Test Title')).toBeInTheDocument();
    expect(screen.getByText('ORD-123')).toBeInTheDocument();
  });
});
```

### Hook mocking

```typescript
import * as UseQuery from 'mystique/hooks/useQuery';
import * as UseAuth from 'mystique/hooks/useAuth';
import { renderHook } from '@testing-library/react-hooks';

afterEach(() => jest.restoreAllMocks());

it('fetches data on mount', () => {
  jest.spyOn(UseQuery, 'useQuery').mockReturnValue([
    { data: { orders: { edges: [] } }, fetching: false, stale: false, error: undefined },
    jest.fn()
  ] as any);

  jest.spyOn(UseAuth, 'useAuth').mockReturnValue({
    loggedIn: true,
    user: { username: 'test-user', roles: [] },
    context: { current: { contextId: '1', contextType: 'RETAILER' }, roleCheck: () => ({ hasRole: true }) },
    logout: jest.fn(),
    switchContext: jest.fn()
  } as any);

  const { result } = renderHook(() => UseQuery.useQuery('query', {}));
  expect(result.current[0].data).toBeDefined();
});
```

OOTB reference plugins prefer `jest.spyOn(...)` plus package-local helpers such as `src/util/TestUtils.tsx` or `src/utils/TestUtils.tsx`, not broad `jest.mock('mystique/...')` replacement. This keeps individual tests explicit and allows sequential `mockImplementationOnce(...)` setup for components that call `useQuery()` more than once.

### Mystique Jest Bootstrap

Mystique tests need more than `@testing-library/jest-dom`. The OOTB setup relies on a small runtime shim:

- `jest.config.js` `setupFiles`: load `lib/material-ui.js`, `lib/init.js`, and `lib/mystique.js`
- `jest.config.js` `moduleNameMapper`: map `^mystique` to `lib/mystique-export.js`
- `src/setup-tests.ts`: keep lightweight (`@testing-library/jest-dom`, optional matcher extensions)

Without that bootstrap, Mystique imports resolve in application code but fail in Jest because the host-provided globals (`window.MaterialUI`, `window.Mystique`) are missing.

### Field Test Fixtures

For `FieldRegistry` tests, pass realistic `FormFieldProps` rather than bare `value/onChange` pairs. The important OOTB props are `entityContext`, `source`, `defaultValue`, `error`, and `lastSubmit`:

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

### Snapshot Policy

- Default to behavioral assertions with `@testing-library/react`
- Use Storybook stories for visual states (`Default`, `Empty`, `Error`, `Disabled`, `EdgeCase`)
- Use Storyshots for snapshot coverage when you need stable visual regression checks
- Treat `react-test-renderer` as Storyshots plumbing, not the default component-test API
- Wrap stories/tests with `FixMuiClassnames` (or equivalent `StylesProvider`) when snapshot stability matters

### Running tests

```bash
yarn test              # Run all tests
yarn test --watch      # Watch mode
yarn test --coverage   # Coverage report
```

### Storybook

```bash
yarn storybook
```

Sample story:

```typescript
// MyComponent.stories.tsx
import MyComponent from './MyComponent';

export default {
  title: 'Components/MyComponent',
  component: MyComponent
};

const data = {
  ref: '1b26dd62-e39b-11eb-b162-0d6b79e73ffc',
  customer: {
    firstname: 'Michael',
    lastname: 'Jordan'
  }
};

export const MyComponentEx = (): JSX.Element => (
  <MyComponent data={data} detail="{{customer.firstname}} {{customer.lastname}}" />
);

MyComponentEx.storyName = 'MyComponent';
```

OOTB plugins use Storybook for two related jobs: local visual review and Storyshots-based snapshot coverage. The common snapshot path is a `.storybook/Storyshots.test.ts` file wired to `@storybook/addon-storyshots`; direct `react-test-renderer` component snapshots are not the main pattern in the reference plugins.

### Additional OOTB Test Layers

- `src/index.test.ts` verifies registry exposure (`ComponentRegistry.register`, `FieldRegistry.register`, plugin bootstrapping)
- `manifests/Manifest.test.ts` validates manifest structure, GraphQL query syntax, and `i18n:` key coverage
- Complex components often keep local `TestUtils.tsx` helpers for stable MUI class names and reusable hook mocks

---

## 9. Build and Deploy

### Building for production

```bash
yarn build
```

Output in `dist/` directory:

```
dist/
├── version.json                      # Version placeholder (short cache TTL)
├── bundle.2779edc9bf9e11ac1a41.js   # Bundled plugin with content hash
```

### Hosting

- Copy `dist/` contents to a publicly hosted web server.
- `version.json` should have a **short cache TTL** (seconds). All other files should have a **long cache TTL** (hours/days).
- When a folder path is added to the manifest plugins config, Mystique loads `<folder>/version.json` first, which contains the name of the latest bundle hash.

### Plugin URL configuration

Update the manifest `plugins` array from localhost to production URL:

```json
{
  "plugins": [
    { "type": "url", "src": "https://cdn.example.com/my-plugin/v1.2.0/" }
  ]
}
```

---

## 10. Naming Conventions (MANDATORY)

All three registries use the same `<namespace>.<segment>.<kebab-name>` dot-separated pattern:

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

### Rules

1. **Namespace** — identifies WHO built the component. Never use `fc.` prefix (reserved for Fluent OOTB). See derivation table below.
2. **Segment** — `content`, `page`, `layout`, `filter` for ComponentRegistry; `fields` for FieldRegistry; `helpers` for TemplateRegistry.
3. **Name** — kebab-case, descriptive. Match the component/field directory name.
4. **No camelCase, no PascalCase** in aliases — always kebab-case for the name segment.
5. **One canonical alias** per component/field. Do NOT register multiple aliases unless migrating from a legacy name.

### Namespace Derivation

| Context | Namespace | Example Aliases |
|---------|-----------|----------------|
| **Fluent OOTB** | `fc` (RESERVED — never use) | `fc.list`, `fc.page` |
| **Partner / SI** | Company abbreviation (2-6 chars) | `acme.content.order-card` |
| **Client / brand** | Brand abbreviation | `supermart.content.loyalty-card` |
| **Feature-scoped** | Feature name | `appeasement.fields.amount-calculator` |

One namespace per plugin bundle. Once deployed, the namespace is baked into manifests and workflows — renaming requires updating all references. Ask the partner/client what prefix they prefer.

### OOTB aliases (platform-provided, `fc.` prefix)

| Scope | Pattern | Example |
|-------|---------|---------|
| First-party | `fc.<type>.<name>` | `fc.card.attribute`, `fc.list`, `fc.page` |

### Field type mapping (workflow ↔ FieldRegistry)

The workflow attribute `type` must exactly match the FieldRegistry key:

```
Workflow attribute type: "appeasement.fields.amount-calculator"
FieldRegistry key:       "appeasement.fields.amount-calculator"
```

| Workflow `attributes[].type` | FieldRegistry key |
|---|---|
| `STRING` / `TEXT` | `text` |
| `INTEGER` / `NUMBER` | `number` |
| `BOOLEAN` | `boolean` |
| `DATE` | `date` |
| `DROPDOWN` | `dropdown` |
| `JSON` | `json` |
| Custom (e.g., `appeasement.fields.amount-calculator`) | Exact same string as registration key |

---

## Best Practices Summary

1. **Prefer page query data** over direct `useQuery` to minimise requests per page. Exception: FieldRegistry fields in user action dialogs MUST use `useQuery()` (page data is unavailable).
2. **Use settings** for dynamic/global configuration; never hardcode API keys or endpoints.
3. **Wrap `onChange` in `useEffect`** in all field components to prevent infinite loops.
4. **Use `./` prefix** for local imports in registration files.
5. **Handle loading and error states** for all data-fetching components.
6. **Type components** with TypeScript interfaces -- avoid `any`.
7. **Use Fluent/Material-UI components** for visual consistency with the platform.
8. **Use `<DynamicValue />`** for maximum user configurability -- attributes can render text, images, or embedded components.
9. **Use `<Children />`** in wrapper components to allow manifest configurers to nest other components inside.
10. **Use `<Children dataOverride={...} />`** in data-transformation wrappers to pass modified data to descendants.
11. **All user-facing text** should use i18n with English defaults.
12. **Never access `data` properties directly** when possible -- use `TemplateRegistry.render(value, data)` so the component is reusable across different data shapes.
