# @middag-io/react

Shared React UI library for the MIDDAG ecosystem. Consumed by Moodle (`local_middag`) and WordPress host plugins via Inertia.js.

**[Live Demo](https://ui-demo.middag.io)** | **[Documentation](https://ui-docs.middag.io)**

## Stack

- React 19, TypeScript, Tailwind CSS v4
- Inertia.js (peer dependency — all hosts use Inertia)
- ReUI components (Radix-based)
- TanStack Table, @xyflow/react, @dnd-kit

## Install

### Quick start (recommended)

```bash
# Community (no auth needed)
npx create-middag-ui

# PRO (source maps, mock SPA, full exports)
npx @middag-io/create-middag-ui
```

The wizard detects your host platform (Moodle/WordPress), configures authentication, and scaffolds a `ui/` directory with a working mock build.

### Manual install

**npm public (no auth needed):**

```bash
npm install @middag-io/react react react-dom @inertiajs/react @inertiajs/core
```

**GitHub Packages (includes TypeScript source for IDE navigation):**

```bash
# Add to global ~/.npmrc
echo "@middag-io:registry=https://npm.pkg.github.com" >> ~/.npmrc
echo "//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN" >> ~/.npmrc

npm install @middag-io/react react react-dom @inertiajs/react @inertiajs/core
```

Create a token at [github.com/settings/tokens](https://github.com/settings/tokens) with `read:packages` scope. See [Authentication](https://ui-docs.middag.io/authentication) for details.

> **GitHub Packages access requires:** membership in the [middag-io](https://github.com/middag-io) organization on GitHub. The token alone is not sufficient — you must have read access to the `middag-io/middag-react` repository.

### Community vs PRO

| Content                            | Community | PRO |
|------------------------------------|-----------|-----|
| ESM bundle (`dist-lib/`)           | Yes       | Yes |
| TypeScript declarations (`.d.ts`)  | Yes       | Yes |
| Declaration maps (`.d.ts.map`)     | Yes       | Yes |
| TypeScript source (`src/`)         | No        | Yes |
| Mock SPA extensible (`mock/`)      | No        | Yes |
| IDE "go to definition" into source | No        | Yes |
| Authentication required            | No        | Yes |

Both editions deliver the same runtime code. Community is sufficient for building plugins. PRO additionally includes TypeScript source for IDE navigation and the extensible mock SPA.

## Usage

```tsx
import { ContractPage, registerDefaults } from '@middag-io/react';
import type { PageContract } from '@middag-io/react';

// Register core shells, layouts, and all 17 blocks (5 lazy-loaded)
registerDefaults();

// Render a contract-driven page
<ContractPage contract={myContract} />
```

### Selective registration (IIFE consumers)

WordPress and other IIFE consumers should import only the blocks they need to avoid bundling heavy dependencies (@xyflow/react, @dnd-kit, zod, react-hook-form) via `inlineDynamicImports`:

```tsx
import {
  registerShell, registerLayout, registerBlock,
  ProductShell, StackLayout, SidebarLayout, DashboardLayout,
  DenseTableBlock, MetricCardBlock, EmptyStateBlock,
} from '@middag-io/react';

registerShell('product', ProductShell);
registerLayout('stack', StackLayout);
registerLayout('sidebar', SidebarLayout);
registerLayout('dashboard', DashboardLayout);
registerBlock('dense_table', DenseTableBlock);
registerBlock('metric_card', MetricCardBlock);
registerBlock('empty_state', EmptyStateBlock);
```

### Lazy block loading

Blocks can defer data loading until they mount (useful for tabbed pages):

```tsx
// PHP sends block with empty data + lazyProp metadata:
{
  type: 'dense_table',
  key: 'invoices',
  data: {},
  meta: { lazyProp: 'invoices' }
}

// PHP also sends a top-level Inertia prop (initially null):
// 'invoices' => null

// When the block mounts, it auto-fetches via router.reload({ only: ['invoices'] })
// Radix Tabs unmounts inactive tabs, so lazy blocks only fetch when their tab activates
```

### ReUI components

Consumers can import ReUI primitives for custom components:

```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@middag-io/react/reui/tabs.tsx';
import { Button } from '@middag-io/react/reui/button.tsx';
```

### Custom blocks

```tsx
import { registerBlock, type BlockProps } from '@middag-io/react';

function ChartBlock({ block }: BlockProps<{ labels: string[]; values: number[] }>) {
  return <div>{/* your chart */}</div>;
}

registerBlock('chart', ChartBlock);
```

### Contract validation

```tsx
import { validatePageContract } from '@middag-io/react';

const errors = validatePageContract(contractFromBackend);
if (errors) {
  console.error('Invalid contract:', errors);
}
```

Zod schemas are exported for consumers who want to extend validation or generate JSON Schema for PHP consumers.

### i18n with host-specific resolver

```tsx
import { I18nProvider } from '@middag-io/react';

// Moodle: inject Moodle string resolver
<I18nProvider asyncResolver={moodleGetStrings}>
  <App />
</I18nProvider>

// WordPress: inject WP i18n resolver
<I18nProvider asyncResolver={wpGetStrings}>
  <App />
</I18nProvider>
```

## Architecture

```
src/
  app/           # ContractPage, registries (shell/layout/block), providers, LazyBlock
  base/          # Shells, layouts, blocks, hooks, theme, form, partials, registries (field/icon/cell)
  components/
    reui/        # ReUI primitives (Radix-based, source of truth)
    examples/    # ReUI component examples (synced from registry)
  contracts/     # TypeScript types (PageContract, BlockData, etc.)
  lib/           # Generic utilities and hooks
  assets/        # Fonts, lottie animations
  index.ts       # Barrel export
mock/            # Dev playground (standalone SPA, no server needed)
scripts/         # Tooling (sync, doctor)
.githooks/       # Git hooks (pre-commit, commit-msg, pre-push)
.changeset/      # Changesets config for automated versioning
```

### What belongs here vs host plugins

| Here (@middag-io/react)     | Host plugin (Moodle/WP)             |
|-----------------------------|-------------------------------------|
| Shells, layouts, blocks     | Extensions with host-specific pages |
| ContractPage renderer       | Inertia server-side adapter         |
| Theme system                | License validation (server-side)    |
| i18n provider (generic)     | i18n resolver (host-specific)       |
| Type contracts              | API endpoints, DB queries           |
| ReUI examples (reference)   | Custom components                   |
| Mock build (dev playground) | AMD build (Moodle), ESM bundle (WP) |

### Extension model

Extensions implement `ExtensionPage` (extends `PageContract`) for licensed features. The build gate excludes unlicensed extensions at compile time. Host plugins handle server-side license checks and only send contracts for active extensions.

## Development

```bash
# Check environment
npm run doctor

# Mock build -- standalone SPA, no server needed
npm run dev:mock        # http://localhost:5174

# Typecheck
npm run typecheck

# Lint
npm run lint:fix

# Build ESM lib
npm run build
```

### Host switching (mock only)

The mock build includes a host switcher (M/W button in sidebar footer) to preview the UI inside Moodle Boost header or WordPress admin chrome.

### ReUI component examples

Examples from the ReUI registry are synced to `src/components/examples/`. They serve as reference for component usage and are excluded from the published NPM package.

```bash
# Sync all examples from ReUI registry (auto-discovers new components)
npm run sync:examples

# Preview what would be synced
npm run sync:examples -- --dry-run

# Check if new examples are available (used by CI)
npm run sync:examples -- --check
```

A GitHub Action runs weekly (Mondays 8:00 UTC) to check for new examples and opens a PR automatically if updates are available.

## Git Hooks

Configured via `.githooks/` (activated by `npm run prepare`):

| Hook         | Trigger      | Protection                           |
|--------------|--------------|--------------------------------------|
| `pre-commit` | `git commit` | Typecheck + lint on staged files     |
| `commit-msg` | `git commit` | Enforces Conventional Commits format |
| `pre-push`   | `git push`   | Full typecheck + lib build           |

## Versioning

Uses [Changesets](https://github.com/changesets/changesets) for automated versioning and changelog with GitHub-linked entries.

```bash
# After finishing a feature:
npx changeset           # select patch/minor/major, write summary
git add . && git commit  # include the .changeset/*.md file

# On merge to main:
# GitHub Action opens "Version Packages" PR
# Merge that PR to publish to GitHub Packages
```

## CLI

The package includes a CLI for bootstrapping and maintaining the UI layer in consumer projects:

| Command                                 | Description                                                |
|-----------------------------------------|------------------------------------------------------------|
| `npx create-middag-ui`                  | Bootstrap `ui/` (Community)                                |
| `npx @middag-io/create-middag-ui`       | Bootstrap `ui/` (PRO)                                      |
| `npx @middag-io/react doctor`           | Validate consumer project setup (deps, configs, peer deps) |
| `npx @middag-io/react dev`              | Start mock dev server from consumer project                |
| `npx @middag-io/react add-block <type>` | Scaffold a new block type with component + mock factory    |
| `npx @middag-io/react upgrade`          | Check for updates and run codemods                         |

## Scripts

| Command                 | Description                           |
|-------------------------|---------------------------------------|
| `npm run doctor`        | Validate development environment      |
| `npm run dev:mock`      | Dev server with hot reload (mock SPA) |
| `npm run build`         | Build ESM lib to `dist-lib/`          |
| `npm run build:mock`    | Build mock SPA to `dist-mock/`        |
| `npm run typecheck`     | TypeScript type check                 |
| `npm run lint`          | ESLint check                          |
| `npm run lint:fix`      | ESLint auto-fix                       |
| `npm run format`        | Prettier format                       |
| `npm run format:check`  | Prettier check                        |
| `npm run sync:examples` | Sync ReUI examples from registry      |
| `npm run changeset`     | Create a changeset for versioning     |
| `npm run release`       | Build + publish (used by CI)          |

## Peer Dependencies

- `react` ^19.0.0
- `react-dom` ^19.0.0
- `@inertiajs/react` ^2.0.0
- `@inertiajs/core` ^2.0.0

## Mock Build Deployment (Cloudflare Pages)

The mock SPA is deployed automatically to Cloudflare Pages on every push to `main`.

**Live URL:** `https://ui-demo.middag.io`

### First-time setup

1. Create the Cloudflare Pages project:
   ```bash
   npx wrangler pages project create middag-react-mock --production-branch main
   ```

2. Add repository secrets in GitHub (`Settings > Secrets and variables > Actions`):
   - `CLOUDFLARE_API_TOKEN` — API token with "Cloudflare Pages: Edit" permission
   - `CLOUDFLARE_ACCOUNT_ID` — your Cloudflare account ID (found in dashboard URL or Workers & Pages overview)

3. Push to `main` — the `deploy-mock.yml` workflow runs automatically.

### Manual deploy (local)

```bash
npm run build:mock
npx wrangler pages deploy dist-mock --project-name=middag-react-mock
```

## CI/CD

| Workflow                       | Trigger                                   | What it does                                                   |
|--------------------------------|-------------------------------------------|----------------------------------------------------------------|
| `publish.yml`                  | Push to main                              | Typecheck + Changesets publish to GitHub Packages + npm public |
| `publish-create-middag-ui.yml` | Push to main (packages/create-middag-ui/) | Publish to npm (Community) + GitHub Packages (PRO)             |
| `deploy-mock.yml`              | Push to main / manual                     | Build mock + deploy to Cloudflare Pages                        |
| `sync-examples.yml`            | Monday 8:00 UTC / manual                  | Sync ReUI examples, opens PR if updates                        |

### Required secrets

| Secret                  | Registry         | Purpose                                         |
|-------------------------|------------------|-------------------------------------------------|
| `GITHUB_TOKEN`          | GitHub Packages  | Publish `@middag-io/react` (auto-provided)      |
| `NPM_TOKEN`             | npm (public)     | Publish `@middag-io/react` + `create-middag-ui` |
| `CLOUDFLARE_API_TOKEN`  | Cloudflare Pages | Deploy mock SPA                                 |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare Pages | Account identifier                              |

## License

**Tiered.** Two packages, two licenses:

- **`@middag-io/react`** (this library — npm public + GitHub Packages): **Apache-2.0 OSS**. The
  rendering engine, all blocks, shells, layouts, contracts and registries. Free to use, fork and
  ship. See [`LICENSE`](./LICENSE).
- **`@middag-io/react-pro`** (GitHub Packages, MIDDAG team only): **proprietary / UNLICENSED**.
  The reserved developer-experience toolkit. Access-gated by GitHub Packages auth — no runtime
  licensing.
- **`mock/`**: not published. The MIDDAG demo pages / dev harness, reserved in-repo.

The premium tier reserves the **developer experience** (the MIDDAG pages and dev toolkit), not the
component code — there is no runtime license gate.
