> ## Documentation Index
> Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt
> Use this file to discover all available pages before exploring further.

# React Components

> Declarative React wrappers and UI primitives shipped with @launchmystore/app-bridge-react

# React Components

`@launchmystore/app-bridge-react` ships **two kinds** of components:

1. **Chrome wrappers** — `<TitleBar />`, `<NavigationMenu />`,
   `<Loading />`. These render nothing visible. They render to the host
   chrome by calling the matching App Bridge action under the hood.
2. **In-iframe UI primitives** — `<AdminBlock>`, `<AdminAction>`,
   `<AdminPrintAction>`, layout/text/form primitives. These render
   inside your extension iframe and give you a consistent look without
   pulling in a separate design system.

All components require an `<AppBridgeProvider>` ancestor — see
[React Hooks](/app-bridge/react-hooks#appbridgeprovider).

## Chrome wrappers

These are declarative shells around their matching hooks. They return
`null` and re-render the host chrome whenever their props change. Use
them when you'd rather think in JSX than in imperative `.update()`
calls.

### `<TitleBar />`

Sets the admin page title, primary/secondary action buttons, and
breadcrumbs. Wraps [`useTitleBar`](/app-bridge/react-hooks#usetitlebar).

```tsx theme={null}
import { TitleBar } from '@launchmystore/app-bridge-react';

export default function ProductPage({ product }) {
  const [saving, setSaving] = useState(false);

  return (
    <>
      <TitleBar
        title={product.title}
        breadcrumbs={[{ label: 'Products', url: '/products' }]}
        primaryAction={{
          label: 'Save',
          loading: saving,
          onAction: async () => {
            setSaving(true);
            await saveProduct(product);
            setSaving(false);
          },
        }}
        secondaryActions={[
          { label: 'Duplicate', onAction: duplicate },
          { label: 'Delete', destructive: true, onAction: remove },
        ]}
      />
      <ProductForm product={product} />
    </>
  );
}
```

**Props (`TitleBarProps`)**

| Prop               | Type                                                      | Description                                                 |
| ------------------ | --------------------------------------------------------- | ----------------------------------------------------------- |
| `title`            | `string`                                                  | Required. Shown as the page title in the host admin chrome. |
| `primaryAction`    | `{ label, onAction?, disabled?, loading?, destructive? }` | Right-aligned filled button.                                |
| `secondaryActions` | `Array<{ label, onAction?, disabled?, destructive? }>`    | Plain buttons left of the primary.                          |
| `breadcrumbs`      | `Array<{ label, url? \| destination? }>`                  | Rendered as a chevron-separated trail before the title.     |

Re-renders trigger an `update()` on the underlying action — flip
`loading: true` on the primary action to show a spinner without
re-creating the bar.

### `<NavigationMenu />`

Registers a left-rail navigation menu for the app's admin pages. Wraps
[`useNavigationMenu`](/app-bridge/react-hooks#usenavigationmenu).

```tsx theme={null}
import { NavigationMenu } from '@launchmystore/app-bridge-react';
import { useRouter } from 'next/router';

export default function AppLayout({ children }) {
  const router = useRouter();
  return (
    <>
      <NavigationMenu
        items={[
          { label: 'Dashboard', url: '/' },
          { label: 'Campaigns', url: '/campaigns', badge: 'New' },
          { label: 'Settings', url: '/settings' },
        ]}
        active={router.pathname}
        onNavigate={(url) => router.push(url)}
      />
      {children}
    </>
  );
}
```

**Props (`NavigationMenuProps`)**

| Prop         | Type                                              | Description                                                           |
| ------------ | ------------------------------------------------- | --------------------------------------------------------------------- |
| `items`      | `Array<{ label, url, disabled?, icon?, badge? }>` | Required. Order = render order.                                       |
| `active`     | `string`                                          | URL of the currently-active item — host highlights it.                |
| `onNavigate` | `(url: string) => void`                           | Called when the user clicks an item. Use this to push to your router. |

The host swallows the click and fires this callback — you're
responsible for the in-iframe route change (e.g. `router.push(url)`).

### `<Loading />`

Toggles the global loading bar in the host chrome (the thin progress
strip at the top of the admin). Wraps
[`useLoading`](/app-bridge/react-hooks#useloading).

```tsx theme={null}
import { Loading } from '@launchmystore/app-bridge-react';

function ProductDetail({ id }) {
  const { data, isLoading } = useAppQuery(`/api/v1/products/${id}`);

  return (
    <>
      <Loading loading={isLoading} />
      {data ? <ProductView product={data} /> : null}
    </>
  );
}
```

**Props (`LoadingProps`)**

| Prop      | Type      | Default | Description                                                                         |
| --------- | --------- | ------- | ----------------------------------------------------------------------------------- |
| `loading` | `boolean` | `true`  | When `true`, the host loading bar is shown. The bar hides on unmount automatically. |

### Contextual save bar

There's no `<ContextualSaveBar />` component — the save bar's
imperative API (`setSaveLoading`, `setDiscardLoading`) is a poor fit
for declarative JSX. Use the
[`useContextualSaveBar`](/app-bridge/react-hooks#usecontextualsavebar--usedirtystate)
or [`useDirtyState`](/app-bridge/react-hooks#usecontextualsavebar--usedirtystate)
hooks instead:

```tsx theme={null}
import { useDirtyState, TextField } from '@launchmystore/app-bridge-react';

function SettingsForm({ initial }) {
  const [form, setForm] = useState(initial);
  const { setDirty } = useDirtyState({
    onSave: () => saveSettings(form),
    onDiscard: () => setForm(initial),
  });

  return (
    <TextField
      label="Store name"
      value={form.name}
      onChange={(name) => {
        setForm((s) => ({ ...s, name }));
        setDirty(true);
      }}
    />
  );
}
```

## Admin extension containers

Use these as the outermost element of any extension iframe. They give
you a card / modal / print layout that matches the host admin look
without you having to import a design system.

### `<AdminBlock>`

Outer container for **block** extension targets (`product.details.block`,
`order.details.block`, etc.). Renders a white card with a title and
padding.

```tsx theme={null}
import { AdminBlock, BlockStack, Text, Badge } from '@launchmystore/app-bridge-react';

export default function Reviews() {
  return (
    <AdminBlock title="Reviews">
      <BlockStack gap="200">
        <Text variant="headingLg">4.8 / 5</Text>
        <Text tone="subdued">124 customer reviews</Text>
        <Badge tone="success">Verified app</Badge>
      </BlockStack>
    </AdminBlock>
  );
}
```

**Props (`AdminBlockProps`)** — `title?: string`, `padding?: number` (default 16), plus any `HTMLDivElement` attributes.

### `<AdminAction>`

Container for **modal-style** action extension targets
(`admin.order-details.action.render`, etc.). Renders a scrolling body
with a sticky footer containing the primary + secondary action
buttons. The footer handles button states (loading / destructive /
disabled) for you.

```tsx theme={null}
import { AdminAction, BlockStack, TextField } from '@launchmystore/app-bridge-react';

export default function RefundDialog({ order, onClose }) {
  const [amount, setAmount] = useState('');
  const [submitting, setSubmitting] = useState(false);

  return (
    <AdminAction
      primaryAction={{
        content: 'Issue refund',
        destructive: true,
        loading: submitting,
        onAction: async () => {
          setSubmitting(true);
          await issueRefund(order.id, amount);
          onClose();
        },
      }}
      secondaryActions={[
        { content: 'Cancel', onAction: onClose },
      ]}
    >
      <BlockStack gap="300">
        <TextField
          label="Refund amount"
          value={amount}
          onChange={setAmount}
        />
      </BlockStack>
    </AdminAction>
  );
}
```

**Props (`AdminActionProps`)**

* `primaryAction?: { content, onAction?, loading?, destructive?, disabled? }`
* `secondaryActions?: Array<{ content, onAction?, disabled? }>`
* Plus any `HTMLDivElement` attributes.

### `<AdminPrintAction>`

Container for **print** extension targets
(`admin.order-details.print.render`). Renders a full-page print
layout with `@media print` rules that reset margins and colors. Use
`usePrintReady()` from inside the tree to signal the host that the
content has finished loading and can be sent to the printer.

```tsx theme={null}
import { AdminPrintAction, usePrintReady, Heading, Text } from '@launchmystore/app-bridge-react';

export default function PackingSlip({ order }) {
  const { setReady } = usePrintReady();
  const { data } = useAppQuery(`/api/v1/orders/${order.id}`);
  useEffect(() => { if (data) setReady(true); }, [data]);

  if (!data) return null;
  return (
    <AdminPrintAction>
      <Heading level={1}>Packing slip</Heading>
      <Text>Order #{data.order_number}</Text>
    </AdminPrintAction>
  );
}
```

The host inspects `data-print-ready="true"` on the container and only
then calls the browser print dialog.

## Layout primitives

Token-driven flex layouts. Gaps use the spacing scale `"0" | "100" |
"200" | "300" | "400" | "500"` (= `0 | 4 | 8 | 12 | 16 | 24` px).

| Component       | Purpose                                            |
| --------------- | -------------------------------------------------- |
| `<BlockStack>`  | Vertical flex column. `gap` defaults to `"200"`.   |
| `<InlineStack>` | Horizontal flex row. `gap`, `align`, `blockAlign`. |
| `<Box>`         | Generic `<div>` with token padding / background.   |
| `<Divider>`     | Thin horizontal rule using `tokens.color.border`.  |

```tsx theme={null}
<BlockStack gap="300">
  <Text variant="headingMd">Order #1234</Text>
  <InlineStack gap="200" align="space-between">
    <Text>Total</Text>
    <Text fontWeight="semibold">$99.00</Text>
  </InlineStack>
  <Divider />
</BlockStack>
```

## Typography

| Component   | Purpose                                                                                                                                                        |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `<Text>`    | Body / heading text. `variant` (`bodyXs`–`bodyLg`, `headingSm`–`headingXl`), `tone` (`base`, `subdued`, `critical`, `success`, `caution`), `fontWeight`, `as`. |
| `<Heading>` | `h1`–`h6` with `level: 1-6`. Wraps `<Text>` with sensible defaults.                                                                                            |
| `<Link>`    | Anchor with token color + hover underline. Pass `external` for `target="_blank" rel="noreferrer"`.                                                             |

```tsx theme={null}
<Heading level={2}>Inventory</Heading>
<Text tone="subdued">Last synced 3 minutes ago.</Text>
<Link external href="https://launchmystore.io/docs">Read the docs</Link>
```

## Display

| Component             | Purpose                                                                                                         |
| --------------------- | --------------------------------------------------------------------------------------------------------------- |
| `<Banner>`            | Tone-coloured callout. `tone: 'info' \| 'warning' \| 'critical' \| 'success'`, `title`, `onDismiss`.            |
| `<Badge>`             | Pill label. `tone` (`info`, `success`, `warning`, `critical`, `attention`, `new`), `size: 'small' \| 'medium'`. |
| `<Image>`             | `<img>` with rounded corners + lazy-loading default.                                                            |
| `<Icon>`              | Inline SVG icon. Pass a `source` SVG path or use the small built-in set.                                        |
| `<EmptyState>`        | Centered "nothing here yet" block with optional `<Button>` action.                                              |
| `<ProgressIndicator>` | Determinate progress bar. `value: 0-100`.                                                                       |
| `<Spinner>`           | Indeterminate circular spinner. `size: 'small' \| 'large'`.                                                     |

```tsx theme={null}
<Banner tone="critical" title="Sync failed" onDismiss={dismiss}>
  Couldn't reach the upstream provider. We'll retry in 5 minutes.
</Banner>
```

## Form

Controlled inputs. All call `onChange(value)` with the next value
(no event object). All accept `label`, `helpText`, `error`.

| Component       | Purpose                                                                                                                                                        |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `<TextField>`   | `<input>` / `<textarea>` (`multiline`). Supports `prefix` / `suffix` slots, `type: 'text' \| 'email' \| 'password' \| 'search' \| 'tel' \| 'url' \| 'number'`. |
| `<NumberField>` | `TextField` with `inputMode="numeric"` + numeric onChange. `min`, `max`, `step`.                                                                               |
| `<MoneyField>`  | `TextField` with currency prefix. `currency: 'USD'` (etc.).                                                                                                    |
| `<DateField>`   | `<input type="date">` returning ISO `YYYY-MM-DD`.                                                                                                              |
| `<Select>`      | Native `<select>`. `options: Array<{ label, value }>`.                                                                                                         |
| `<Checkbox>`    | Single boolean.                                                                                                                                                |
| `<ChoiceList>`  | Radio (single) or checkbox (`allowMultiple`) group. `choices: Array<{ label, value, helpText? }>`.                                                             |
| `<Button>`      | `variant: 'primary' \| 'secondary' \| 'tertiary' \| 'plain'`, `tone: 'base' \| 'critical' \| 'success'`, `size`, `loading`.                                    |
| `<Form>`        | `<form>` that calls `onSubmit(e)` and prevents default. Useful for `Enter` to submit.                                                                          |

```tsx theme={null}
import { Form, TextField, MoneyField, Select, Button, BlockStack } from '@launchmystore/app-bridge-react';

function CreateDiscount() {
  const [code, setCode] = useState('');
  const [amount, setAmount] = useState('10');
  const [kind, setKind] = useState('percentage');

  return (
    <Form onSubmit={() => createDiscount({ code, amount, kind })}>
      <BlockStack gap="300">
        <TextField
          label="Discount code"
          value={code}
          onChange={setCode}
          helpText="Customers enter this at checkout"
        />
        <Select
          label="Kind"
          value={kind}
          onChange={setKind}
          options={[
            { label: 'Percentage off', value: 'percentage' },
            { label: 'Fixed amount off', value: 'fixed' },
          ]}
        />
        {kind === 'percentage'
          ? <NumberField label="Percent off" value={amount} onChange={setAmount} min={0} max={100} />
          : <MoneyField label="Amount off" value={amount} onChange={setAmount} currency="USD" />}
        <Button variant="primary">Create</Button>
      </BlockStack>
    </Form>
  );
}
```

## Design tokens

All components read from a shared token set exported by
`@launchmystore/app-bridge-react`:

* `tokens.color.*` — base, surface, border, primary, critical, success,
  info, warning, plus `*Subdued` / `*Border` variants for banners.
* `tokens.font.*` — `family`, `size.{xs,sm,md,lg,xl}`, `weight.*`,
  `lineHeight.*`.
* `tokens.radius.*` — `sm` (4), `md` (6), `lg` (8).
* `tokens.shadow.*` — `sm` for cards.

You can override per-component via the standard `style` prop — the
token style is spread first, your `style` last.

## When to use components vs. hooks

| Goal                                              | Use                                     |
| ------------------------------------------------- | --------------------------------------- |
| Set the title bar declaratively                   | `<TitleBar />`                          |
| Toggle the title bar's `primaryAction.loading`    | Either — the component re-renders.      |
| Open a confirmation modal once on click           | `useConfirmationModal` hook             |
| Render a scrolling-with-sticky-footer action page | `<AdminAction>`                         |
| Show / hide the host loading bar                  | `<Loading loading={...} />`             |
| Manage a contextual save bar tied to form state   | `useDirtyState` hook                    |
| Open a resource picker imperatively               | `useResourcePicker` / `useApi().picker` |

Both styles use the same wire protocol — pick whichever reads better
in the component you're writing.

## See Also

* [React Hooks](/app-bridge/react-hooks) — every hook the components
  wrap, plus the data-fetching / cart / lifecycle hooks that have no
  component equivalent.
* [Actions Reference](/app-bridge/actions) — raw payload shapes for
  every App Bridge action.
* [App Bridge Overview](/app-bridge/overview) — the iframe ↔ host
  postMessage contract underneath all of this.
